├── .cargo └── config.toml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assemble_demo.mp4 ├── assets ├── cube.glb ├── models │ ├── motor.glb │ └── multi_primitive_model.glb └── robots │ └── diff_bot │ ├── models │ ├── base.glb │ ├── caster_wheel.glb │ └── wheel.glb │ └── urdf │ ├── cube.xml │ └── diff_bot.xml ├── crates ├── bevy_assemble │ ├── Cargo.toml │ ├── examples │ │ ├── model_load_tests.rs │ │ └── robot_serialization.rs │ ├── saves │ │ └── diff_bot.xml │ └── src │ │ ├── components.rs │ │ ├── gltf │ │ ├── OLD │ │ │ ├── components.rs │ │ │ ├── mod.rs │ │ │ └── system.rs │ │ ├── mod.rs │ │ ├── physics │ │ │ ├── khr_implicit_shapes │ │ │ │ ├── khr_implicit_shapes.rs │ │ │ │ └── mod.rs │ │ │ ├── khr_physics_rigid_bodies │ │ │ │ ├── extension.rs │ │ │ │ ├── mod.rs │ │ │ │ └── node.rs │ │ │ └── mod.rs │ │ └── synonyms.rs │ │ ├── lib.rs │ │ ├── plugins.rs │ │ ├── resources.rs │ │ ├── systems.rs │ │ ├── traits.rs │ │ └── urdf │ │ ├── loader.rs │ │ ├── mod.rs │ │ ├── resources.rs │ │ ├── urdf.rs │ │ └── visual.rs ├── bevy_synonymize │ ├── Cargo.toml │ ├── examples │ │ └── synonymization.rs │ └── src │ │ ├── lib.rs │ │ ├── plugins.rs │ │ ├── resources.rs │ │ ├── synonyms │ │ ├── material │ │ │ └── mod.rs │ │ ├── mesh │ │ │ └── mod.rs │ │ └── mod.rs │ │ ├── systems.rs │ │ └── traits.rs ├── bevy_synonymize_physics │ ├── Cargo.toml │ ├── examples │ │ └── bevy_rapier_example.rs │ └── src │ │ ├── lib.rs │ │ ├── plugins.rs │ │ ├── synonyms │ │ ├── colliders.rs │ │ ├── collisiongroupfilter.rs │ │ ├── continous_collision.rs │ │ ├── friction.rs │ │ ├── link.rs │ │ ├── mass.rs │ │ ├── mod.rs │ │ ├── rigidbodies.rs │ │ └── solvergroupfilter.rs │ │ └── systems.rs └── bevy_synonymize_save │ ├── Cargo.toml │ ├── examples │ └── save_load.rs │ ├── saves │ ├── green.ron │ └── red.ron │ └── src │ ├── lib.rs │ ├── plugins.rs │ ├── resources.rs │ └── systems.rs ├── demo_gif.webm ├── diff_bot ├── edit_example.png └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | linker = "clang" 3 | rustflags = ["-C", "link-arg=-fuse-ld=mold"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./crates/bevy_synonymize_physics/Cargo.toml", 4 | "./crates/bevy_synonymize/Cargo.toml", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # bevy_serialization_extras 0.9 4 | 5 | bevy_serialization_extras has now been updated to 0.9! 6 | 7 | This is a big update with a lot of refactors, new features, and quality life improvements. 8 | 9 | # changes 10 | 11 | 12 | ### bevy_assemble refactors: 13 | 14 | bevy_assemble's 0.1 release had a lot of rough edges around it. So, this update has been mostly focused on improvements to it. 15 | 16 | #### renamed FromStructure into Disassemble, and impl refactors 17 | 18 | [`FromStructure`] has been renamed to [`Disassemble`] to match the new composition over inheritance approach for asset deserialization. No longer are assets deserialized from one giant function, but instead, decomposed into: 19 | 20 | - their constituent components 21 | - further [`DisassembleAssetRequest`]s 22 | 23 | 24 | World access based initialization was a source of many footguns and fights with the borrow checker. 25 | 26 | For disassembly, structures are disassembled into a `Structure` that holds either the components of the structure, or a collection of bundles which hold the components of its children. 27 | 28 | E.G: A request is made to spawn a robot via urdf 29 | 30 | ```rust 31 | // request to spawn a robot saved in the URDF format. 32 | commands.spawn(( 33 | RequestAssetStructure::::Path("root://model_pkg/urdf/diff_bot.xml".to_owned()), 34 | Transform::from_xyz(-2.0, 0.0, 0.0), 35 | )); 36 | ``` 37 | 38 | and this wrapper is disassembled into either its constituent components, or its constituent children. 39 | 40 | ```rust 41 | impl Disassemble for UrdfWrapper { 42 | fn components(value: Self) -> Structure { 43 | let mut structured_joint_map = HashMap::new(); 44 | 45 | for joint in &value.0.joints { 46 | structured_joint_map.insert(joint.child.link.clone(), joint.clone()); 47 | } 48 | 49 | let mut linkage = Vec::new(); 50 | for link in value.0 .0.links { 51 | linkage.push(( 52 | link.clone(), 53 | structured_joint_map 54 | .get_key_value(&link.name) 55 | .map(|(_, joint)| joint.clone()), 56 | )) 57 | } 58 | Structure::Root(( 59 | Name::new(value.0 .0.name), 60 | RequestStructure(LinksNJoints(linkage)), 61 | )) 62 | } 63 | } 64 | ... 65 | 66 | #[derive(Clone, Deref)] 67 | pub struct LinksNJoints(Vec<(Link, Option)>); 68 | 69 | impl Disassemble for LinksNJoints { 70 | fn components(value: Self) -> Structure { 71 | let mut children = Vec::new(); 72 | 73 | for (link, joint) in value.0 { 74 | let joint = joint.map(|n| DisassembleRequest(UrdfJoint(n)), 75 | ); 76 | children.push(( 77 | Name::new(link.name), 78 | DisassembleRequest(LinkColliders(link.collision)), 79 | Maybe(joint), 80 | Visibility::default(), 81 | )) 82 | } 83 | Structure::Children(children, Split(true)) 84 | } 85 | } 86 | ``` 87 | 88 | So on and so forth untill all `DisassembleRequest`s and `DisassembleAssetRequest`s have been disassembled into their underlying components. 89 | 90 | 91 | 92 | ### Assset saving systems implemented, and IntoHashMap renamed into [`Assemble`] 93 | 94 | - 95 | IntoHashMap is now [`Assemble`], and Assembling assets is now properly implemented! 96 | 97 | Previously, the systems in charge for this were skeletons, but they have now been properly implemented to allow asset saving! Implement [`Assemble`] on an asset wrapper around your asset, and you can save into that asset's file format! 98 | 99 | See crate's `/urdf` module for example impl 100 | 101 | 102 | - 103 | Post impl, assets can now be saved via the [`AssembleRequest`] resource on `Assemble` implementing asset synonyms E.G to serialize a urdf: 104 | ```rust 105 | let request = AssembleRequest::::new( 106 | // file name 107 | "diff_bot".into(), 108 | // assetSource string literal id 109 | "saves".to_string(), 110 | // selected entities to save into asset 111 | entities.clone(), 112 | ); 113 | ``` 114 | 115 | ### bevy_synonymize refactors: 116 | 117 | #### trait bound consolidation 118 | wrapper components have had their trait bounds consolidated under the new [`WrapperComponent`] trait. 119 | 120 | wrapper components around assets have had their trait bounds consolidated under [`AssetSynonym`] 121 | 122 | ### Mandatory pure/path variants for synonyms + Simplified trait impls 123 | 124 | [`FromWrapper`] has been removed in favor of From impls from your asset wrapper to asset directly. 125 | 126 | In exchange, [`AssetSynonym`]s must now be enums with a `PureVariant` and path variant (from string). 127 | 128 | E.G: 129 | 130 | ```rust 131 | #[derive(Component, Reflect, From)] 132 | #[reflect(Component)] 133 | pub enum Mesh3dFlag { 134 | Path(String), 135 | Pure(MeshWrapper), 136 | } 137 | ``` 138 | 139 | ### bevy_synonymize_physics refactors: 140 | 141 | - AsyncColliderFlag has been removed in favor of [`ColliderFlag`]. If you wish to initialize AsyncColliderFlag, use AsyncCollider from rapier. 142 | 143 | - [`JointFlag`] no longer has a name attribute to prevent truth desync -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_serialization_extras" 3 | version = "0.10.0-beta.0" 4 | edition = "2024" 5 | repository = "https://github.com/rydb/bevy_serialization_extras" 6 | readme = "README.md" 7 | license = "MIT" 8 | description = "A crate for managing serialization with moonshine_save + synonyms." 9 | 10 | [workspace] 11 | members = [ 12 | "crates/*" 13 | ] 14 | 15 | [workspace.dependencies] 16 | anyhow = "1" 17 | bevy-inspector-egui = "0.31.0" 18 | bytemuck = "1.22" 19 | bevy_camera_extras = "0.16" 20 | bevy_ui_extras = "0.20" 21 | ref-cast = "1.0" 22 | gltf = "1.4" 23 | serde_json = {version = "1"} 24 | serde = {version = "1"} 25 | bevy_rapier3d = "0.30" 26 | # bevy_rapier3d = {git = "https://github.com/mnmaita/bevy_rapier", rev = "98da7beae596001a6daf59a62d3e1e19adb4811b"} 27 | rapier3d = "0.25.0" 28 | repr-trait = "1.0" 29 | bevy = "0.16" 30 | bevy_ecs = "0.16" 31 | bevy_input = "0.16" 32 | bevy_render = "0.16" 33 | bevy_app = "0.16" 34 | const_format = "0.2" 35 | bevy_reflect = "0.16" 36 | bevy_pbr = "0.16" 37 | bevy_utils = "0.16" 38 | bevy_math = "0.16" 39 | bevy_log = "0.16" 40 | bevy_tasks = "0.16" 41 | bevy_core_pipeline = "0.16" 42 | bevy_asset = "0.16" 43 | bevy_internal = {version = "0.16", features = ["serialize"]} 44 | bevy_gltf = "0.16" 45 | bevy_transform = "0.16" 46 | bevy_window = "0.16" 47 | bevy_picking = "0.16" 48 | bevy_derive = "0.16" 49 | bevy_color = "0.16" 50 | bevy_state = "0.16" 51 | moonshine-save = "0.4.1" 52 | glam = "0.29" 53 | ron = "0.10" 54 | urdf-rs = "0.9.0" 55 | multimap = "0.10" 56 | strum = "0.27" 57 | thiserror = "2.0" 58 | strum_macros = "0.27" 59 | bitflags = "2.4.1" 60 | nalgebra = "0.33" 61 | yaserde = "0.12" 62 | derive_more = {version = "2", features = ["from"]} 63 | bitvec = "1.0" 64 | log = "0.4.2" 65 | 66 | bevy_synonymize = {path = "./crates/bevy_synonymize", version = "0.8.0-beta.0"} 67 | bevy_synonymize_save = {path = "./crates/bevy_synonymize_save"} 68 | bevy_synonymize_physics = {path = "./crates/bevy_synonymize_physics", version = "0.8.0-beta.0"} 69 | bevy_assemble = {path = "./crates/bevy_assemble", version = "0.3.0-beta.0"} 70 | 71 | [dependencies] 72 | bevy_synonymize = {workspace = true} 73 | bevy_synonymize_save = {workspace = true} 74 | bevy_synonymize_physics = {optional = true, workspace = true} 75 | bevy_assemble = {optional = true, workspace = true} 76 | 77 | 78 | 79 | [features] 80 | default = ["physics", "assemble"] 81 | physics = ["bevy_synonymize_physics"] 82 | assemble = ["bevy_assemble"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 rydb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

bevy_serialization_extras

4 |

5 | A library for component oriented serialization. 6 | 7 |

8 | 9 | 10 | ## Features 11 | 12 | - Out of the box serialization Through [`plugins`] for components 13 | 14 | ```Rust 15 | // Component <-> WrapperComponent 16 | .add_plugins(SynonymizeComponent::::default()) 17 | 18 | // Asset <-> WrapperComponent 19 | .add_plugins(SynonymizeAsset::, MaterialFlag3d>::default()) 20 | // Query -> Component, 21 | .add_plugins(SerializeQueryFor::::default()) 22 | ``` 23 | 24 | - Serialization of groups of enities that compose an asset into their singular asset equivillent, and vice-versa 25 | 26 | ### A visualization util to list serializable/unserializable components(W.I.P) [bevy_synonymize] 27 | 28 | [demo_gif.webm](https://github.com/rydb/bevy_serialization_extras/assets/43288084/3bda45f1-c75a-437b-a02d-27e58bd3276e) 29 | 30 | ### Visualize and edit 3rd party components that do not [`Reflect`] 31 | #### E.G: Edit Rapier's `ImpulseJoint` through `JointFlag` [bevy_synonymize_physics] 32 | ![edit_example.png](edit_example.png) 33 | 34 | ### Serialize a collection of entities into an [`Asset`] that is composed of them 35 | #### E.G: serialize the parts of a robot into a [`Urdf`] [bevy_assemble] 36 | 37 | ```Rust 38 | //(entity_0, ... entity_n) -> Asset 39 | // [UNIMPLEMENTED] Asset -> (entity_0, ... entity_n) 40 | .add_plugins(SerializeManyAsOneFor::::default()) 41 | ``` 42 | 43 | https://github.com/user-attachments/assets/fb1a1b09-db3f-4476-9b0d-800b296ccb8a 44 | 45 | 46 | ## Why bevy_serialization_extras? 47 | 48 | - bevy_serialization_extras is built ontop of `bevy_reflect`, not serde. No need to double dip to serialize. 49 | 50 | - bevy_serialization_extras allows regular serialization into .ron via [`moonshine_save`] + allows converting serializables into more stable file formats via bevy_assemble. 51 | 52 | serde serialization: 53 | > world <--> scene.json 54 | 55 | bevy_serialziation_extras: 56 | ``` 57 | (object) <--> (partA, partB) <--> .file 58 | (person) (body + arm_0...arm_n + leg_0..leg_n) <--> .file 59 | world <--> scene.ron 60 | ``` 61 | 62 | This is good for creating editors in bevy. 63 | 64 | ## Credits 65 | 66 | [`moonshine_save`] for the save/load backend of this library 67 | 68 | 69 | ## Usage 70 | 71 | See sub-crate `/examples` files for example usage. 72 | -------------------------------------------------------------------------------- /assemble_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assemble_demo.mp4 -------------------------------------------------------------------------------- /assets/cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/cube.glb -------------------------------------------------------------------------------- /assets/models/motor.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/models/motor.glb -------------------------------------------------------------------------------- /assets/models/multi_primitive_model.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/models/multi_primitive_model.glb -------------------------------------------------------------------------------- /assets/robots/diff_bot/models/base.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/robots/diff_bot/models/base.glb -------------------------------------------------------------------------------- /assets/robots/diff_bot/models/caster_wheel.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/robots/diff_bot/models/caster_wheel.glb -------------------------------------------------------------------------------- /assets/robots/diff_bot/models/wheel.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/assets/robots/diff_bot/models/wheel.glb -------------------------------------------------------------------------------- /assets/robots/diff_bot/urdf/cube.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/robots/diff_bot/urdf/diff_bot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /crates/bevy_assemble/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_assemble" 3 | version = "0.3.0-beta.0" 4 | edition = "2024" 5 | repository = "https://github.com/rydb/bevy_serialization_extras" 6 | license = "MIT" 7 | description = "A crate for assembling assets from entities and components" 8 | 9 | 10 | [dependencies] 11 | anyhow = {workspace = true} 12 | bevy_synonymize = {workspace = true} 13 | bevy_synonymize_physics = {workspace = true} 14 | bevy_ecs = {workspace = true} 15 | bevy_transform = {workspace = true} 16 | bevy_render = {workspace = true} 17 | bevy_reflect = {workspace = true} 18 | bevy_color = {workspace = true} 19 | bevy_utils = {workspace = true} 20 | bevy_state = {workspace = true} 21 | bevy_asset = {workspace = true} 22 | ref-cast = {workspace = true} 23 | bevy_app = {workspace = true} 24 | bevy_internal = {workspace = true} 25 | serde_json = {workspace = true} 26 | serde = {workspace = true} 27 | gltf = {workspace = true} 28 | bevy_pbr = {workspace = true} 29 | bytemuck = {workspace = true} 30 | bevy_log = {workspace = true} 31 | bevy_derive = {workspace = true} 32 | bevy_tasks = {workspace = true} 33 | bevy_gltf = {workspace = true} 34 | thiserror = {workspace = true} 35 | derive_more = {workspace = true} 36 | glam = {workspace = true} 37 | nalgebra = {workspace = true} 38 | urdf-rs = {workspace = true} 39 | bevy_math = {workspace = true} 40 | moonshine-save = {workspace = true} 41 | strum_macros = {workspace = true} 42 | strum = {workspace = true} 43 | bevy_rapier3d = {workspace = true} 44 | 45 | [dev-dependencies] 46 | bevy = {workspace = true, features = [ 47 | "dynamic_linking", 48 | "trace_tracy" 49 | ]} 50 | const_format = {workspace = true} 51 | bevy_ui_extras = {workspace = true} 52 | moonshine-save = {workspace = true} 53 | bevy_camera_extras = {workspace = true} 54 | bevy_rapier3d = {workspace = true} 55 | bevy-inspector-egui = {workspace = true} 56 | -------------------------------------------------------------------------------- /crates/bevy_assemble/examples/model_load_tests.rs: -------------------------------------------------------------------------------- 1 | //! A simple 3D scene with light shining over a cube sitting on a plane. 2 | 3 | use bevy::prelude::*; 4 | use bevy_asset::io::{ 5 | AssetSource, 6 | file::{FileAssetReader, FileAssetWriter}, 7 | }; 8 | use bevy_camera_extras::{CameraController, CameraExtrasPlugin, CameraRestrained}; 9 | use bevy_rapier3d::{plugin::RapierPhysicsPlugin, render::RapierDebugRenderPlugin}; 10 | use bevy_assemble::{ 11 | components::DisassembleAssetRequest, 12 | gltf::{physics::GltfPhysicsPlugin, synonyms::GltfModel}, 13 | prelude::*, 14 | }; 15 | use bevy_synonymize::prelude::*; 16 | use bevy_synonymize_physics::prelude::*; 17 | use bevy_ui_extras::{UiExtrasDebug, visualize_components_for}; 18 | use moonshine_save::save::Save; 19 | 20 | pub const SAVES: &str = "saves"; 21 | 22 | fn main() { 23 | App::new() 24 | .add_plugins(AppSourcesPlugin::CRATE) 25 | .add_plugins(AssetSourcesUrdfPlugin { 26 | //TODO: This should be unified underc `ROOT` 27 | assets_folder_local_path: "../../assets".to_owned(), 28 | }) 29 | .add_plugins(DefaultPlugins.set(WindowPlugin { 30 | exit_condition: bevy::window::ExitCondition::OnPrimaryClosed, 31 | ..Default::default() 32 | })) 33 | .add_plugins(RapierPhysicsPlugin::<()>::default()) 34 | .add_plugins(RapierDebugRenderPlugin::default()) 35 | // // serialization plugins 36 | .add_plugins(SerializationPlugin) 37 | .add_plugins(SerializationAssembleBasePlugin) 38 | .add_plugins(SynonymizePhysicsPlugin) 39 | .add_plugins(SerializationBasePlugin) 40 | .add_plugins(GltfPhysicsPlugin) 41 | .add_plugins(UrdfSerializationPlugin) 42 | // // rapier physics plugins 43 | .add_plugins(UiExtrasDebug { 44 | menu_mode: bevy_ui_extras::states::DebugMenuState::Explain, 45 | ..default() 46 | }) 47 | .add_plugins(CameraExtrasPlugin { 48 | cursor_grabbed_by_default: true, 49 | ..default() 50 | }) 51 | // .add_systems( 52 | // Update, 53 | // visualize_components_for::(bevy_ui_extras::Display::Side( 54 | // bevy_ui_extras::Side::Right, 55 | // )), 56 | // ) 57 | // // Demo systems 58 | .add_systems(Startup, setup) 59 | .run(); 60 | } 61 | 62 | #[derive(Component)] 63 | pub struct BasePlate; 64 | 65 | #[derive(Component, Clone, Reflect)] 66 | pub struct Selected; 67 | 68 | /// set up a simple 3D scene 69 | fn setup( 70 | mut commands: Commands, 71 | mut meshes: ResMut>, 72 | mut materials: ResMut>, 73 | assets: Res, 74 | ) { 75 | // plane 76 | commands.spawn(( 77 | Mesh3d(meshes.add(Plane3d::new( 78 | Vec3::new(0.0, 1.0, 0.0), 79 | Vec2::new(50.0, 50.0), 80 | ))), 81 | MeshMaterial3d(materials.add(Color::LinearRgba(LinearRgba::new(0.3, 0.5, 0.3, 1.0)))), 82 | Transform::from_xyz(0.0, -1.0, 0.0), 83 | RigidBodyFlag::Fixed, 84 | RequestCollider::Convex, 85 | Name::new("plane"), 86 | BasePlate, 87 | )); 88 | 89 | // Physics enabled gltf 90 | commands.spawn(( 91 | DisassembleAssetRequest::::path("root://models/motor.glb".to_owned(), None), 92 | Name::new("model"), 93 | )); 94 | // light 95 | commands.spawn(( 96 | PointLight { 97 | intensity: 1500.0, 98 | shadows_enabled: true, 99 | ..default() 100 | }, 101 | Transform::from_xyz(4.0, 8.0, 4.0), 102 | Save, 103 | )); 104 | // camera 105 | commands.spawn(( 106 | Camera3d::default(), 107 | Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 108 | CameraController { 109 | restrained: CameraRestrained(false), 110 | camera_mode: bevy_camera_extras::CameraMode::Free, 111 | }, 112 | )); 113 | } 114 | 115 | pub const ROOT: &str = "root"; 116 | 117 | /// Whether this is a crate or `main.rs`. 118 | pub enum AppSourcesPlugin { 119 | CRATE, 120 | MAIN, 121 | } 122 | 123 | impl Plugin for AppSourcesPlugin { 124 | fn build(&self, app: &mut App) { 125 | let executor_location = match *self { 126 | Self::CRATE => "../../", 127 | Self::MAIN => "./", 128 | }; 129 | app.register_asset_source( 130 | ROOT, 131 | AssetSource::build().with_reader(move || { 132 | Box::new(FileAssetReader::new( 133 | executor_location.to_owned() + "assets", 134 | )) 135 | }), 136 | ); 137 | app.register_asset_source( 138 | SAVES, 139 | AssetSource::build() 140 | .with_reader(move || Box::new(FileAssetReader::new(SAVES))) 141 | .with_writer(move |create_root| { 142 | Some(Box::new(FileAssetWriter::new(SAVES, create_root))) 143 | }), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /crates/bevy_assemble/examples/robot_serialization.rs: -------------------------------------------------------------------------------- 1 | //! A simple 3D scene with light shining over a cube sitting on a plane. 2 | 3 | use std::{any::TypeId, collections::HashSet}; 4 | 5 | use bevy::{prelude::*, window::PrimaryWindow}; 6 | use bevy_asset::io::{ 7 | AssetSource, 8 | file::{FileAssetReader, FileAssetWriter}, 9 | }; 10 | use bevy_camera_extras::{CameraController, CameraExtrasPlugin, CameraRestrained}; 11 | use bevy_inspector_egui::{ 12 | bevy_egui::EguiContext, 13 | egui::{self, Align2}, 14 | }; 15 | use bevy_rapier3d::{plugin::RapierPhysicsPlugin, render::RapierDebugRenderPlugin}; 16 | use bevy_assemble::{ 17 | Assemblies, JointRequest, SaveSuccess, 18 | components::{DisassembleAssetRequest, DisassembleStage}, 19 | prelude::*, 20 | traits::DisassembleSettings, 21 | }; 22 | use bevy_synonymize::prelude::*; 23 | use bevy_synonymize_physics::prelude::*; 24 | use bevy_ui_extras::{DEBUG_FRAME_STYLE, UiExtrasDebug, visualize_components_for}; 25 | use moonshine_save::save::Save; 26 | 27 | use strum_macros::{Display, EnumIter}; 28 | 29 | pub const SAVES: &str = "saves"; 30 | pub const ROBOT: &str = "diff_bot"; 31 | 32 | fn main() { 33 | App::new() 34 | .add_plugins(AppSourcesPlugin::CRATE) 35 | .add_plugins(AssetSourcesUrdfPlugin { 36 | //TODO: This should be unified under `ROOT` 37 | assets_folder_local_path: "../../assets".to_owned(), 38 | }) 39 | .add_plugins( 40 | DefaultPlugins.set(WindowPlugin { 41 | exit_condition: bevy::window::ExitCondition::OnPrimaryClosed, 42 | ..Default::default() 43 | }), //.set(bevy_mod_raycast::low_latency_window_plugin()) 44 | ) 45 | .insert_state(InitializationStage::Select) 46 | //.add_schedule(Schedule::new(AssetCheckSchedule)) 47 | .insert_resource(SetSaveFile { 48 | name: "blue".to_owned(), 49 | }) 50 | .insert_resource(UtilitySelection::default()) 51 | .add_plugins(RapierPhysicsPlugin::<()>::default()) 52 | .add_plugins(RapierDebugRenderPlugin::default()) 53 | // // serialization plugins 54 | .add_plugins(SerializationPlugin) 55 | .add_plugins(SerializationAssembleBasePlugin) 56 | .add_plugins(SynonymizePhysicsPlugin) 57 | .add_plugins(SerializationBasePlugin) 58 | .add_plugins(UrdfSerializationPlugin) 59 | // // rapier physics plugins 60 | .add_plugins(UiExtrasDebug { 61 | menu_mode: bevy_ui_extras::states::DebugMenuState::Explain, 62 | ..default() 63 | }) 64 | .add_plugins(CameraExtrasPlugin { 65 | cursor_grabbed_by_default: true, 66 | ..default() 67 | }) 68 | .add_systems( 69 | Update, 70 | visualize_components_for::(bevy_ui_extras::Display::Side( 71 | bevy_ui_extras::Side::Right, 72 | )), 73 | ) 74 | // // Demo systems 75 | .register_type::() 76 | .add_systems(Startup, setup) 77 | .add_systems(Update, control_robot) 78 | .add_systems(Update, bind_left_and_right_wheel) 79 | // .add_systems(Update, freeze_spawned_robots) 80 | .add_systems( 81 | Update, 82 | select_robot.run_if(in_state(InitializationStage::Select)), 83 | ) 84 | .add_systems( 85 | Update, 86 | save_selected_robot.run_if(in_state(InitializationStage::Save)), 87 | ) 88 | .add_systems( 89 | Update, 90 | load_saved_robot.run_if(in_state(InitializationStage::LoadSaved)), 91 | ) 92 | .run(); 93 | } 94 | 95 | #[derive(States, Debug, PartialEq, PartialOrd, Hash, Eq, Clone)] 96 | pub enum InitializationStage { 97 | Select, 98 | Save, 99 | LoadSaved, 100 | } 101 | 102 | #[derive(Component)] 103 | pub struct BasePlate; 104 | 105 | #[derive(Component, Clone, Reflect)] 106 | pub struct Selected; 107 | 108 | #[derive(Component, Reflect, Display)] 109 | pub enum Wheel { 110 | Left, 111 | Right, 112 | Passive, 113 | } 114 | 115 | /// set up a simple 3D scene 116 | fn setup( 117 | mut commands: Commands, 118 | mut meshes: ResMut>, 119 | mut materials: ResMut>, 120 | ) { 121 | // plane 122 | commands.spawn(( 123 | Mesh3d(meshes.add(Plane3d::new( 124 | Vec3::new(0.0, 1.0, 0.0), 125 | Vec2::new(50.0, 50.0), 126 | ))), 127 | MeshMaterial3d(materials.add(Color::LinearRgba(LinearRgba::new(0.3, 0.5, 0.3, 1.0)))), 128 | Transform::from_xyz(0.0, -1.0, 0.0), 129 | RigidBodyFlag::Fixed, 130 | RequestCollider::Convex, 131 | Name::new("plane"), 132 | BasePlate, 133 | )); 134 | // let robot = "diff_bot.xml"; 135 | // Robot1 136 | commands.spawn(( 137 | DisassembleAssetRequest::( 138 | DisassembleStage::Path("root://robots/diff_bot/urdf/".to_owned() + ROBOT + ".xml"), 139 | DisassembleSettings { split: false }, 140 | ), 141 | Transform::from_xyz(-2.0, 0.0, 0.0), 142 | )); 143 | 144 | // light 145 | commands.spawn(( 146 | PointLight { 147 | intensity: 1500.0, 148 | shadows_enabled: true, 149 | ..default() 150 | }, 151 | Transform::from_xyz(4.0, 8.0, 4.0), 152 | Save, 153 | )); 154 | // camera 155 | commands.spawn(( 156 | Camera3d::default(), 157 | Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 158 | CameraController { 159 | restrained: CameraRestrained(false), 160 | camera_mode: bevy_camera_extras::CameraMode::Free, 161 | }, 162 | )); 163 | } 164 | 165 | pub fn select_robot( 166 | parts: Query< 167 | Entity, 168 | ( 169 | With, 170 | With, 171 | With, 172 | With, 173 | Without, 174 | Without, 175 | ), 176 | >, 177 | mut commands: Commands, 178 | mut initialization_stage: ResMut>, 179 | ) { 180 | if parts.iter().len() > 0 { 181 | for part in &parts { 182 | commands.entity(part).insert(Selected); 183 | } 184 | initialization_stage.set(InitializationStage::Save) 185 | } 186 | } 187 | 188 | pub fn save_selected_robot( 189 | selected: Query>, 190 | mut assemble_requests: ResMut>, 191 | mut initialization_stage: ResMut>, 192 | uninitialized_joints: Query<&JointRequest>, 193 | ) { 194 | if selected.iter().len() > 0 { 195 | // do not attempt to save the robot while joints are uninitialized or it won't save correctly! 196 | if uninitialized_joints.iter().len() <= 0 { 197 | let entities = &mut selected.iter().collect::>(); 198 | 199 | println!("selected entities: {:#?}", entities); 200 | let request = AssembleRequest::::new( 201 | ROBOT.into(), 202 | SAVES.to_string(), 203 | entities.clone(), 204 | ); 205 | assemble_requests.0.push(request); 206 | initialization_stage.set(InitializationStage::LoadSaved) 207 | } 208 | } 209 | } 210 | 211 | pub fn load_saved_robot( 212 | mut commands: Commands, 213 | mut event_reader: EventReader, 214 | assemblies: Res, 215 | ) { 216 | for event in event_reader.read() { 217 | if event.asset_type_id == TypeId::of::() { 218 | println!("Loading saved robot: {:#}", event.file_name); 219 | 220 | // Robot2 221 | commands.spawn(( 222 | DisassembleAssetRequest::( 223 | DisassembleStage::Path("saves://".to_owned() + &event.file_name + ".xml"), 224 | DisassembleSettings { split: true }, 225 | ), 226 | Transform::from_xyz(2.0, 0.0, 0.0), 227 | )); 228 | } 229 | println!("Assemblies: {:#?}", assemblies) 230 | } 231 | } 232 | 233 | #[derive(Resource, Default)] 234 | pub struct SetSaveFile { 235 | pub name: String, 236 | } 237 | 238 | #[derive(Default, EnumIter, Display)] 239 | pub enum UtilityType { 240 | #[default] 241 | UrdfInfo, 242 | } 243 | #[derive(Resource, Default)] 244 | pub struct UtilitySelection { 245 | pub selected: UtilityType, 246 | } 247 | 248 | pub const ROOT: &str = "root"; 249 | 250 | /// Whether this is a crate or `main.rs`. 251 | pub enum AppSourcesPlugin { 252 | CRATE, 253 | MAIN, 254 | } 255 | 256 | impl Plugin for AppSourcesPlugin { 257 | fn build(&self, app: &mut App) { 258 | let executor_location = match *self { 259 | Self::CRATE => "../../", 260 | Self::MAIN => "./", 261 | }; 262 | app.register_asset_source( 263 | ROOT, 264 | AssetSource::build().with_reader(move || { 265 | Box::new(FileAssetReader::new( 266 | executor_location.to_owned() + "assets", 267 | )) 268 | }), 269 | ); 270 | app.register_asset_source( 271 | SAVES, 272 | AssetSource::build() 273 | .with_reader(move || Box::new(FileAssetReader::new(SAVES))) 274 | .with_writer(move |create_root| { 275 | Some(Box::new(FileAssetWriter::new(SAVES, create_root))) 276 | }), 277 | ); 278 | } 279 | } 280 | 281 | pub fn control_robot( 282 | mut rigid_body_flag: Query<&mut RigidBodyFlag, Without>, 283 | keys: Res>, 284 | mut primary_window: Query<&mut EguiContext, With>, 285 | mut wheels: Query<(&mut JointFlag, &Wheel)>, 286 | ) { 287 | let target_speed = 20.0; 288 | 289 | let turn_speed_multiplier = 0.5; 290 | let leftward_key = KeyCode::ArrowLeft; 291 | let rightward_key = KeyCode::ArrowRight; 292 | let forward_key = KeyCode::ArrowUp; 293 | let backward_key = KeyCode::ArrowDown; 294 | 295 | let freeze_key = KeyCode::KeyP; 296 | let unfreeze_key = KeyCode::KeyO; 297 | 298 | for mut context in primary_window.iter_mut() { 299 | egui::Window::new("robot controls") 300 | .frame(DEBUG_FRAME_STYLE) 301 | .anchor(Align2::LEFT_BOTTOM, [0.0, 0.0]) 302 | .show(context.get_mut(), |ui| { 303 | ui.label(format!("Freeze key: {:#?}", freeze_key)); 304 | ui.label(format!("unfreeze key {:#?}", unfreeze_key)); 305 | ui.label("-------------------------"); 306 | ui.label(""); 307 | ui.label("wheel controls") 308 | }); 309 | } 310 | for (mut joint, wheel) in wheels.iter_mut() { 311 | for axis in joint.joint.motors.iter_mut() { 312 | if keys.pressed(forward_key) { 313 | axis.target_vel = target_speed 314 | } else if keys.pressed(backward_key) { 315 | axis.target_vel = -target_speed 316 | } else { 317 | axis.target_vel = 0.0 318 | } 319 | } 320 | match wheel { 321 | Wheel::Left => { 322 | for axis in joint.joint.motors.iter_mut() { 323 | if keys.pressed(leftward_key) { 324 | axis.target_vel = -target_speed * turn_speed_multiplier; 325 | } 326 | if keys.pressed(rightward_key) { 327 | axis.target_vel = target_speed * turn_speed_multiplier; 328 | } 329 | } 330 | } 331 | Wheel::Right => { 332 | for axis in joint.joint.motors.iter_mut() { 333 | if keys.pressed(leftward_key) { 334 | axis.target_vel = target_speed * turn_speed_multiplier; 335 | } 336 | if keys.pressed(rightward_key) { 337 | axis.target_vel = -target_speed * turn_speed_multiplier; 338 | } 339 | } 340 | } 341 | Wheel::Passive => {} 342 | } 343 | } 344 | 345 | if keys.pressed(freeze_key) { 346 | for mut rigidbody in rigid_body_flag.iter_mut() { 347 | *rigidbody = RigidBodyFlag::Fixed; 348 | } 349 | } 350 | if keys.pressed(unfreeze_key) { 351 | for mut rigidbody in rigid_body_flag.iter_mut() { 352 | *rigidbody = RigidBodyFlag::Dynamic; 353 | } 354 | } 355 | } 356 | 357 | /// find what is "probably" the left and right wheel, and give them a marker. 358 | pub fn bind_left_and_right_wheel( 359 | robots: Query<(Entity, &Name), (With, Without)>, 360 | mut commands: Commands, 361 | ) { 362 | for (e, name) in robots.iter() { 363 | let name_str = name.to_string().to_lowercase(); 364 | 365 | let split_up = name_str.split("_").collect::>(); 366 | //println!("binding wheel"); 367 | if split_up.contains(&Wheel::Left.to_string().to_lowercase().as_str()) { 368 | commands.entity(e).insert(Wheel::Left); 369 | } else if split_up.contains(&Wheel::Right.to_string().to_lowercase().as_str()) { 370 | commands.entity(e).insert(Wheel::Right); 371 | } else { 372 | commands.entity(e).insert(Wheel::Passive); 373 | } 374 | } 375 | } 376 | 377 | #[derive(Component, Reflect)] 378 | pub struct WasFrozen; 379 | 380 | pub fn freeze_spawned_robots( 381 | mut robots: Query<(Entity, &mut RigidBodyFlag), Without>, 382 | mut commands: Commands, 383 | ) { 384 | for (e, mut body) in robots.iter_mut() { 385 | *body = RigidBodyFlag::Fixed; 386 | commands.entity(e).insert(WasFrozen); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /crates/bevy_assemble/saves/diff_bot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/OLD/components.rs: -------------------------------------------------------------------------------- 1 | // use bevy_ecs::prelude::*; 2 | // use bevy_reflect::Reflect; 3 | 4 | // #[derive(Component, Reflect)] 5 | // pub struct GltfTarget; -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/OLD/mod.rs: -------------------------------------------------------------------------------- 1 | // use std::{fs, io::{self, Write}, mem}; 2 | 3 | // use gltf::json::{self as json}; 4 | 5 | 6 | // use gltf_json::validation::Checked; 7 | // use json::validation::Checked::Valid; 8 | // use json::validation::USize64; 9 | 10 | // use super::{MeshInfo, MeshKind, Vertex}; 11 | 12 | // mod system; 13 | // pub mod components; 14 | // #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 15 | // pub enum Output { 16 | // /// Output standard glTF. 17 | // Standard, 18 | 19 | // // /// Output binary glTF. 20 | // // Binary, 21 | // } 22 | 23 | // struct MeshOut { 24 | // vertices: Vec, 25 | // indices: Vec, 26 | // } 27 | 28 | 29 | // #[derive(Debug)] 30 | // pub enum GltfError { 31 | // // FileNonExistant, 32 | // CouldNotWriteToFile(io::Error), 33 | // SerializationError, 34 | // GltfOutputError, 35 | // BinFileSizeLimitError, 36 | // } 37 | 38 | 39 | // pub fn export( 40 | // mesh_kind: MeshKind, 41 | // output_file: &str, 42 | // output: Output, 43 | // ) -> Result<(), GltfError> { 44 | 45 | // match mesh_kind { 46 | // MeshKind::Path(_) => { 47 | // todo!("GlFX support required for this. Implement later.") 48 | // //export_from_path() 49 | // }, 50 | // MeshKind::Geometry(mesh_info) => { 51 | // export_from_geoemtry(mesh_info, output_file, output) 52 | // }, 53 | // } 54 | 55 | // // if root.accessors == root.accessors { 56 | 57 | // // } 58 | // //Ok(Document(root)) 59 | // } 60 | 61 | 62 | // #[derive(Copy, Clone, Debug, Pod, Zeroable)] 63 | // #[repr(C)] 64 | // pub(crate) struct Vertex { 65 | // position: [f32; 3], 66 | // normal: [f32; 3], 67 | // } 68 | 69 | 70 | 71 | // pub(crate) struct MeshInfo<'a> { 72 | // pub vertices: &'a [[f32; 3]], 73 | // pub normals: &'a [[f32; 3]], 74 | // pub indices: &'a [u16], 75 | // } 76 | 77 | // pub(crate) enum MeshKind<'a> { 78 | // // Path to mesh 79 | // Path(String), 80 | // // Path to mesh 81 | // Geometry(MeshInfo<'a>) 82 | // } 83 | 84 | // // bytemuck crate uses unsafe under the hood. 85 | // fn dynamic_mesh_to_bytes(mesh: &MeshOut) -> Vec { 86 | // let mut bytes = Vec::new(); 87 | // bytes.extend_from_slice(bytemuck::cast_slice(&mesh.vertices)); 88 | // bytes.extend_from_slice(bytemuck::cast_slice(&mesh.indices)); 89 | // bytes 90 | // } 91 | 92 | 93 | // /// Calculate bounding coordinates of a list of vertices, used for the clipping distance of the model 94 | // fn bounding_coords(vertices: &[[f32; 3]]) -> ([f32; 3], [f32; 3]) { 95 | // let mut min = [f32::MAX, f32::MAX, f32::MAX]; 96 | // let mut max = [f32::MIN, f32::MIN, f32::MIN]; 97 | 98 | // for point in vertices.iter() { 99 | // let p = point; 100 | // for i in 0..3 { 101 | // min[i] = f32::min(min[i], p[i]); 102 | // max[i] = f32::max(max[i], p[i]); 103 | // } 104 | // } 105 | // (min, max) 106 | // } 107 | 108 | 109 | // //TODO: Requires rust support for GlFx/spec merge. 110 | // // fn export_from_path( 111 | // // path: String, 112 | // // output_file: &str, 113 | // // output: Output, 114 | // // ) { 115 | 116 | // // } 117 | 118 | // fn export_from_geoemtry( 119 | // mesh_info: MeshInfo, 120 | // output_file: &str, 121 | // output: Output, 122 | // ) -> Result<(), GltfError> { 123 | // let (min_vert, max_vert) = bounding_coords(mesh_info.vertices); 124 | 125 | // let mut root = json::Root::default(); 126 | 127 | // let verticie_byte_length = mem::size_of_val(mesh_info.vertices); 128 | // let normal_byte_length = mem::size_of_val(mesh_info.normals); 129 | // let indicie_byte_length = mem::size_of_val(mesh_info.indices); 130 | 131 | // let byte_total = USize64::from(verticie_byte_length + normal_byte_length + indicie_byte_length); 132 | 133 | // let buffer = root.push(json::Buffer { 134 | // byte_length: byte_total, 135 | // extensions: Default::default(), 136 | // extras: Default::default(), 137 | // name: None, 138 | // uri: if output == Output::Standard { 139 | // Some("buffer0.bin".into()) 140 | // } else { 141 | // None 142 | // }, 143 | // }); 144 | 145 | // // This part of the code configure the json buffer with the glTF format 146 | // // Create buffer views 147 | // // vertex buffer // 148 | // let buffer_view = root.push(json::buffer::View { 149 | // buffer, 150 | // byte_length: USize64::from(verticie_byte_length), 151 | // byte_offset: None, 152 | // byte_stride: None, 153 | // extensions: Default::default(), 154 | // extras: Default::default(), 155 | // name: None, 156 | // target: Some(Valid(json::buffer::Target::ArrayBuffer)), 157 | // }); 158 | 159 | // // vertices accessors 160 | // let verticies_accessor = root.push(json::Accessor { 161 | // buffer_view: Some(buffer_view), 162 | // byte_offset: None, 163 | // count: USize64::from(mesh_info.vertices.len()), 164 | // component_type: Valid(json::accessor::GenericComponentType( 165 | // json::accessor::ComponentType::F32, 166 | // )), 167 | // extensions: Default::default(), 168 | // extras: Default::default(), 169 | // type_: Valid(json::accessor::Type::Vec3), 170 | // min: Some(json::Value::from(Vec::from(min_vert))), 171 | // max: Some(json::Value::from(Vec::from(max_vert))), 172 | // name: None, 173 | // normalized: false, 174 | // sparse: None, 175 | // }); 176 | // let normal_buffer_view = root.push(gltf_json::buffer::View { 177 | // buffer, 178 | // byte_length: USize64::from(normal_byte_length), 179 | // byte_offset: Some(USize64::from(verticie_byte_length)), 180 | // byte_stride: None, 181 | // extensions: Default::default(), 182 | // extras: Default::default(), 183 | // name: None, 184 | // target: Some(Checked::Valid(gltf_json::buffer::Target::ArrayBuffer)), 185 | // }); 186 | 187 | // // normal accessor 188 | // let normal_accessor = root.push(json::Accessor { 189 | // buffer_view: Some(normal_buffer_view), 190 | // byte_offset: None, 191 | // count: USize64::from(mesh_info.normals.len()), 192 | // component_type: Valid(json::accessor::GenericComponentType( 193 | // json::accessor::ComponentType::F32, 194 | // )), 195 | // extensions: Default::default(), 196 | // extras: Default::default(), 197 | // type_: Valid(json::accessor::Type::Vec3), 198 | // min: None, 199 | // max: None, 200 | // name: None, 201 | // normalized: false, 202 | // sparse: None, 203 | // }); 204 | // let index_buffer_view = root.push(gltf_json::buffer::View { 205 | // buffer: buffer, 206 | // byte_length: USize64::from(indicie_byte_length), 207 | // byte_offset: Some(USize64::from(verticie_byte_length + normal_byte_length)), 208 | // byte_stride: None, 209 | // extensions: Default::default(), 210 | // extras: Default::default(), 211 | // name: None, 212 | // target: Some(Checked::Valid(gltf_json::buffer::Target::ElementArrayBuffer)), 213 | // }); 214 | 215 | // let indices_accessor = root.push(json::Accessor { 216 | // buffer_view: Some(index_buffer_view), 217 | // byte_offset: None, 218 | // count: USize64::from(mesh_info.indices.len()), 219 | // component_type: Valid(json::accessor::GenericComponentType( 220 | // json::accessor::ComponentType::U16, 221 | // )), 222 | // extensions: Default::default(), 223 | // extras: Default::default(), 224 | // type_: Valid(json::accessor::Type::Scalar), 225 | // min: None, 226 | // max: None, 227 | // name: None, 228 | // normalized: false, 229 | // sparse: None, 230 | // }); 231 | 232 | 233 | // // Mesh 234 | // let primitive = json::mesh::Primitive { 235 | // attributes: { 236 | // let mut map = std::collections::BTreeMap::new(); 237 | // map.insert(Valid(json::mesh::Semantic::Positions), verticies_accessor); 238 | // map.insert(Valid(json::mesh::Semantic::Normals), normal_accessor); 239 | // map 240 | // }, 241 | // extensions: Default::default(), 242 | // extras: Default::default(), 243 | // indices: Some(indices_accessor), 244 | // material: None, 245 | // mode: Valid(json::mesh::Mode::Triangles), 246 | // targets: None, 247 | // }; 248 | 249 | // let mesh = root.push(json::Mesh { 250 | // extensions: Default::default(), 251 | // extras: Default::default(), 252 | // name: None, 253 | // primitives: vec![primitive], 254 | // weights: None, 255 | // }); 256 | 257 | // let node = root.push(json::Node { 258 | // mesh: Some(mesh), 259 | // ..Default::default() 260 | // }); 261 | 262 | // // Scene 263 | // root.push(json::Scene { 264 | // extensions: Default::default(), 265 | // extras: Default::default(), 266 | // name: None, 267 | // nodes: vec![node], 268 | // }); 269 | 270 | // // Generate the glTF with 2 format options : full binary or a mix of text/binary. 271 | // // serialize the data 272 | 273 | // // Reconstruct the mesh format mesh { vertex, indices} from the "pieces". 274 | // let vertices_vec = mesh_info.vertices.iter().zip(mesh_info.normals.iter()).map(|(pos, norm)| 275 | // Vertex { 276 | // position: *pos, 277 | // normal: *norm, 278 | // }).collect(); 279 | 280 | // let mesh = MeshOut { 281 | // vertices: vertices_vec, 282 | // indices: mesh_info.indices.to_vec(), 283 | // }; 284 | 285 | // match output { 286 | // Output::Standard => { 287 | // let _ = fs::create_dir("glTF_ouput_dir"); 288 | // let path = format!("glTF_ouput_dir/{}.gltf", output_file); 289 | // let writer = match fs::File::create(path) { 290 | // Ok(file) => file, 291 | // Err(e) => return Err(GltfError::CouldNotWriteToFile(e)), 292 | // }; 293 | 294 | // if json::serialize::to_writer_pretty(writer, &root).is_err() { 295 | // return Err(GltfError::SerializationError); 296 | // }; 297 | 298 | // let bin = dynamic_mesh_to_bytes(&mesh); 299 | 300 | // let mut writer = match fs::File::create("glTF_ouput_dir/buffer0.bin") { 301 | // Ok(file) => file, 302 | // Err(e) => return Err(GltfError::CouldNotWriteToFile(e)), 303 | // }; 304 | 305 | // writer 306 | // .write_all(&bin) 307 | // .map_err(GltfError::CouldNotWriteToFile)?; 308 | // } 309 | // } 310 | // Ok(()) 311 | 312 | // } -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/OLD/system.rs: -------------------------------------------------------------------------------- 1 | // use bevy_ecs::prelude::*; 2 | // use bevy_asset::prelude::*; 3 | // use bevy_reflect::Reflect; 4 | // use bevy_render::prelude::*; 5 | // use bevy_core::prelude::*; 6 | 7 | // use log::warn; 8 | 9 | // use crate::prelude::mesh::{gltf::{export, Output}, MeshInfo, MeshKind}; 10 | 11 | // use super::components::GltfTarget; 12 | 13 | 14 | 15 | // /// serialize a mesh from a bevy [`Mesh`] 16 | // pub fn serialize_as_gltf( 17 | // meshes: ResMut>, 18 | // models: Query<(Entity, &Mesh3d, &Name), With>, 19 | // mut commands: Commands, 20 | // ) { 21 | 22 | // let Ok((e, mesh, name)) = models.get_single() 23 | // .inspect_err(|err| { 24 | // match err { 25 | // bevy_ecs::query::QuerySingleError::NoEntities(_) => return, 26 | // bevy_ecs::query::QuerySingleError::MultipleEntities(err) => warn!("test only works with 1 model at a time. Actual error: {:#?}", err), 27 | // } 28 | // }) 29 | // else {return;}; 30 | 31 | // let output_file = "cube"; 32 | // let output = Output::Standard; 33 | 34 | // // export mesh to point to path if it points to one. Else, assume mesh is procedural and export geometry to file. 35 | // let result = match mesh.0.path() { 36 | // Some(path) => { 37 | // export( 38 | // MeshKind::Path(path.path().to_str().unwrap().to_owned()), 39 | // output_file, 40 | // output, 41 | // ) 42 | // }, 43 | // None => { 44 | // let Some(mesh) = meshes.get(mesh) else { 45 | // warn!("mesh not fetchable from handle. Exiting"); 46 | // return; 47 | // }; 48 | // println!("serializing: {:#?}", name); 49 | 50 | // let Some(positions)= mesh.attribute(Mesh::ATTRIBUTE_POSITION) else { 51 | // warn!("Expected positions. Exiting"); 52 | // return; 53 | // }; 54 | // let Some(positions) = positions.as_float3() else { 55 | // warn!("Expected positions ot be float3. Exiting"); 56 | // return; 57 | // }; 58 | 59 | // let Some(normals) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) else { 60 | // warn!("Expected normals. Exiting"); 61 | // return; 62 | // }; 63 | // let Some(normals) = normals.as_float3() else { 64 | // warn!("normals not float3. Exiting"); 65 | // return; 66 | // }; 67 | 68 | // let Some(indices) = mesh.indices() else { 69 | // warn!("Expected indices. Exiting"); 70 | // return; 71 | // }; 72 | 73 | // let indices = indices.iter().map(|i| i as u16).collect::>(); 74 | 75 | // export( 76 | // MeshKind::Geometry( 77 | // MeshInfo { 78 | // vertices: positions, 79 | // normals, 80 | // indices: &indices, 81 | // }, 82 | // ), 83 | // &output_file, 84 | // output 85 | // ) 86 | // }, 87 | // }; 88 | 89 | 90 | // println!("result print result: {:#?}", result); 91 | // commands.entity(e).remove::(); 92 | // } 93 | 94 | // //TODO: Implement this properly later. 95 | // // #[derive(Resource, Default, Deref, DerefMut)] 96 | // // pub struct GltfDocumentImport(pub Option); 97 | 98 | // // #[derive(Resource, Default, Deref, DerefMut)] 99 | // // pub struct GltfDocumentExport(pub Option); 100 | 101 | // // ///serialize a mesh that was imported from elsewhere. 102 | // // pub fn serialize_as_gltf_import( 103 | // // mut gltf_export: ResMut, 104 | // // ) { 105 | // // ////let str = include_str!("../../../assets/correct_cube.gltf"); 106 | // // let file = fs::File::open("assets/correct_cube.gltf").unwrap(); 107 | // // let reader = io::BufReader::new(file); 108 | // // let gltf = gltf::Gltf::from_reader(reader).unwrap(); 109 | 110 | // // **gltf_export = Some(gltf.document); 111 | // // //println!("gltf is {:#?}", gltf); 112 | // // } -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | use bevy_asset::Handle; 3 | use bevy_ecs::prelude::*; 4 | use bevy_gltf::{Gltf, GltfExtras, GltfNode}; 5 | use bevy_log::warn; 6 | use bevy_render::prelude::*; 7 | use bevy_synonymize_physics::prelude::RequestCollider; 8 | use bevy_transform::components::Transform; 9 | use strum::IntoEnumIterator; 10 | 11 | pub mod physics; 12 | pub mod synonyms; 13 | 14 | 15 | pub fn gltf_collider_request(extras: &GltfExtras) -> RequestCollider { 16 | if let Some(target) = extras.value.replace(['}', '{', '"'], "").split(':').last() { 17 | for variant in RequestCollider::iter() { 18 | if target == variant.to_string().to_lowercase() { 19 | return variant.into(); 20 | } 21 | } 22 | warn!( 23 | " 24 | provided GltfExtra attribute did not match any valid primitive colliders. reverting to default 25 | valid variants: {:#?} 26 | parsed value: {:#} 27 | ", RequestCollider::iter().map(|n| n.to_string()).collect::>(), target 28 | ); 29 | }; 30 | 31 | RequestCollider::default() 32 | } 33 | 34 | pub enum SchemaKind { 35 | GLTF, 36 | } 37 | 38 | /// a request to align the transform of this entity to match bevy's cordinate system. 39 | #[derive(Component)] 40 | #[require(Transform)] 41 | pub struct TransformSchemaAlignRequest(pub Transform, pub SchemaKind); 42 | 43 | #[derive(Component, Default)] 44 | pub struct RootNode { 45 | handle: Handle, 46 | } 47 | 48 | /// request to re-align geoemtry to match bevy. 49 | /// TODO: replace with normal Mesh3d if gltf mesh loading is improved to not have this done at the [`Gltf`] level. 50 | #[derive(Component)] 51 | pub struct Mesh3dAlignmentRequest(pub Handle, pub SchemaKind); 52 | 53 | /// Gltf associated with this entity. 54 | #[derive(Component, Clone)] 55 | pub struct GltfAssociation(pub Handle); 56 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/khr_implicit_shapes/khr_implicit_shapes.rs: -------------------------------------------------------------------------------- 1 | //! Rust wrapper around the [`KHR_IMPLICIT_SHAPES`] section of the gltf physics spec proposal. 2 | //! https://github.com/eoineoineoin/glTF_Physics/tree/master/extensions/2.0/Khronos/KHR_implicit_shapes 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | 7 | pub const KHR_IMPLICIT_SHAPES: &'static str = "khr_implicit_shapes"; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct KHRImplicitShapesMap { 12 | pub shapes: Vec, 13 | } 14 | 15 | #[derive(Clone, Debug, Serialize)] 16 | #[serde(tag = "type", rename_all = "lowercase")] 17 | pub enum Shape { 18 | Box(BoxShape), 19 | Cylinder(CylinderShape), 20 | } 21 | 22 | #[derive(Clone, Debug, Serialize, Deserialize)] 23 | pub struct BoxShape { 24 | #[serde(rename = "box")] 25 | pub size: BoxData, 26 | } 27 | 28 | #[derive(Clone, Debug, Serialize, Deserialize)] 29 | pub struct CylinderShape { 30 | #[serde(rename = "cylinder")] 31 | pub dimensions: CylinderData, 32 | } 33 | 34 | #[derive(Clone, Debug, Serialize, Deserialize)] 35 | pub struct BoxData { 36 | pub size: [f64; 3], 37 | } 38 | 39 | #[derive(Clone, Debug, Serialize, Deserialize)] 40 | pub struct CylinderData { 41 | pub height: f64, 42 | pub radius_bottom: f64, 43 | pub radius_top: f64, 44 | } 45 | 46 | impl<'de> Deserialize<'de> for Shape { 47 | fn deserialize(deserializer: D) -> std::result::Result 48 | where 49 | D: serde::Deserializer<'de>, 50 | { 51 | let value: Value = Deserialize::deserialize(deserializer)?; 52 | let shape_type = value 53 | .get("type") 54 | .and_then(Value::as_str) 55 | .ok_or_else(|| serde::de::Error::custom("Missing shape type"))?; 56 | 57 | match shape_type { 58 | "box" => Ok(Shape::Box(BoxShape { 59 | size: BoxData { 60 | size: serde_json::from_value(value["box"]["size"].clone()) 61 | .map_err(serde::de::Error::custom)?, 62 | }, 63 | })), 64 | "cylinder" => Ok(Shape::Cylinder(CylinderShape { 65 | dimensions: CylinderData { 66 | height: value["cylinder"]["height"] 67 | .as_f64() 68 | .ok_or_else(|| serde::de::Error::custom("Missing cylinder height"))?, 69 | radius_bottom: value["cylinder"]["radiusBottom"] 70 | .as_f64() 71 | .ok_or_else(|| serde::de::Error::custom("Missing radiusBottom"))?, 72 | radius_top: value["cylinder"]["radiusTop"] 73 | .as_f64() 74 | .ok_or_else(|| serde::de::Error::custom("Missing radiusTop"))?, 75 | }, 76 | })), 77 | _ => Err(serde::de::Error::custom(format!( 78 | "Unknown shape type: {}", 79 | shape_type 80 | ))), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/khr_implicit_shapes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod khr_implicit_shapes; 2 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/khr_physics_rigid_bodies/extension.rs: -------------------------------------------------------------------------------- 1 | //! Rust wrapper around [`KHR_PHYSICS_RIGID_BODIES`] section of the gltf physics spec proposal. 2 | //! https://github.com/eoineoineoin/glTF_Physics/tree/master/extensions/2.0/Khronos/KHR_physics_rigid_bodies 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub const KHR_PHYSICS_RIGID_BODIES: &'static str = "khr_physics_rigid_bodies"; 7 | 8 | /// KHR_physics_rigid_bodies properties map. 9 | /// proposal. 10 | #[derive(Clone, Serialize, Deserialize, Debug, Default)] 11 | pub struct KhrPhysicsRigidBodiesMap { 12 | #[serde(rename = "physicsMaterials")] 13 | pub physics_materials: Vec, 14 | #[serde(rename = "collisionFilters")] 15 | pub collision_filters: Vec, 16 | } 17 | 18 | #[derive(Clone, Serialize, Deserialize, Default, Debug)] 19 | pub struct CollisionFilters { 20 | #[serde(rename = "collisionSystems")] 21 | pub collision_systems: Vec, 22 | #[serde(rename = "collideWithSystems")] 23 | pub collide_with_systems: Vec, 24 | } 25 | 26 | #[derive(Clone, Serialize, Deserialize, Default, Debug)] 27 | pub struct PhysicsMaterials { 28 | #[serde(rename = "staticFriction")] 29 | pub static_friction: f32, 30 | #[serde(rename = "dynamicFriction")] 31 | pub dynamic_friction: f32, 32 | #[serde(rename = "restitution")] 33 | pub restitution: f32, 34 | } 35 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/khr_physics_rigid_bodies/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod extension; 2 | pub mod node; 3 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/khr_physics_rigid_bodies/node.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// KHRPhysicsRigidBodies property on a node. 4 | #[derive(Serialize, Deserialize, Debug, Default)] 5 | pub struct KHRPhysicsRigidBodiesNodeProp { 6 | #[serde(rename = "motion")] 7 | pub motion: Motion, 8 | #[serde(rename = "collider")] 9 | pub collider: Collider, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Debug, Default)] 13 | pub struct Motion { 14 | pub mass: f32, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Default)] 18 | pub struct Geometry { 19 | #[serde(rename = "shape")] 20 | pub shape_index: usize, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug, Default)] 24 | pub struct Collider { 25 | pub geometry: Geometry, 26 | #[serde(rename = "physicsMaterial")] 27 | pub physics_material: u32, 28 | #[serde(rename = "collisionFilter")] 29 | pub collision_filter: u32, 30 | } 31 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/physics/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bevy_app::prelude::*; 4 | use bevy_asset::prelude::*; 5 | use bevy_derive::{Deref, DerefMut}; 6 | use bevy_ecs::{ 7 | component::{ComponentHooks, Immutable}, 8 | prelude::*, 9 | }; 10 | use bevy_gltf::Gltf; 11 | use bevy_log::warn; 12 | use bevy_math::primitives::{Cuboid, Cylinder}; 13 | use bevy_reflect::Reflect; 14 | use bevy_synonymize::prelude::mesh::MeshPrefab; 15 | use bevy_synonymize_physics::prelude::ColliderFlag; 16 | 17 | pub mod khr_implicit_shapes; 18 | pub mod khr_physics_rigid_bodies; 19 | 20 | use bevy_ecs::component::StorageType; 21 | use glam::Vec3; 22 | use khr_implicit_shapes::khr_implicit_shapes::{KHR_IMPLICIT_SHAPES, KHRImplicitShapesMap, Shape}; 23 | use khr_physics_rigid_bodies::{ 24 | extension::{KHR_PHYSICS_RIGID_BODIES, KhrPhysicsRigidBodiesMap}, 25 | node::KHRPhysicsRigidBodiesNodeProp, 26 | }; 27 | 28 | use crate::AssemblyId; 29 | 30 | use super::{GltfAssociation, synonyms::NodeId}; 31 | 32 | #[derive(Default, Clone)] 33 | pub struct PhysicsProperties { 34 | //pub colliders: Vec, 35 | pub implicit_shapes: KHRImplicitShapesMap, 36 | pub physics_rigid_bodies: KhrPhysicsRigidBodiesMap, 37 | } 38 | 39 | /// Request for the physics of a given node from the [`GltfPhysicsRegistry`] 40 | #[derive(Component, Reflect)] 41 | pub struct NodePhysicsRequest(pub Vec<(usize, NodePhysicsMap)>); 42 | 43 | #[derive(Reflect)] 44 | pub struct NodePhysicsMap { 45 | pub motion: f32, 46 | pub collider_id: usize, 47 | } 48 | 49 | /// plugin for initializing infastructure for gltf physics 50 | pub struct GltfPhysicsPlugin; 51 | 52 | impl Plugin for GltfPhysicsPlugin { 53 | fn build(&self, app: &mut App) { 54 | app.init_resource::() 55 | .register_type::() 56 | .add_systems(Update, bind_physics); 57 | } 58 | } 59 | 60 | /// component holding a request to register specific gltf's physics properties. 61 | #[derive(Clone)] 62 | pub struct GltfPhysicsRegistration { 63 | pub physics: PhysicsProperties, 64 | } 65 | 66 | impl Component for GltfPhysicsRegistration { 67 | const STORAGE_TYPE: StorageType = StorageType::SparseSet; 68 | 69 | fn register_component_hooks(_hooks: &mut ComponentHooks) { 70 | _hooks.on_add(|mut world, hook| { 71 | let registration = world.get::(hook.entity).unwrap().clone(); 72 | 73 | let Some(gltf_handle) = world.get::(hook.entity).map(|n| n.clone()) 74 | else { 75 | warn!( 76 | "{:#?} requested gltf physics but has no associated gltf. Skipping.", 77 | hook.entity 78 | ); 79 | return; 80 | }; 81 | println!("gltf physics initialized for: {:#?}", gltf_handle.0); 82 | let mut gltf_physics_registry = 83 | world.get_resource_mut::().unwrap(); 84 | 85 | gltf_physics_registry.insert(gltf_handle.0, registration.physics); 86 | 87 | world.commands().entity(hook.entity).remove::(); 88 | }); 89 | } 90 | 91 | type Mutability = Immutable; 92 | } 93 | 94 | /// Registry for pyhsics properties for a given gltf. 95 | /// [`Gltf`] does not hold handles to non-intrinsic extensions. This is where gltf physics are accessed. 96 | #[derive(Resource, Default, Deref, DerefMut)] 97 | pub struct PhysicsPropertyRegistry(pub HashMap, PhysicsProperties>); 98 | 99 | /// parse gltf physics extensions into their extension property maps. 100 | pub fn parse_gltf_physics( 101 | gltf: &gltf::Gltf, 102 | ) -> Result<(PhysicsProperties, Vec<(usize, NodePhysicsMap)>), String> { 103 | // let error_message = &gltf.document.as_json(); 104 | let Some(external_extensions) = gltf.extensions() else { 105 | return Err("gltf external extensions evaluated to none".to_owned()); 106 | }; 107 | let Some((khr_physics_rigid_bodies_key, khr_physics_rigid_bodies_values)) = external_extensions 108 | .iter() 109 | .find(|(n, val)| n.eq_ignore_ascii_case(KHR_PHYSICS_RIGID_BODIES)) 110 | else { 111 | return Err(format!( 112 | "could not find khr_physics_rigid_bodies in extensions: {:#?}", 113 | external_extensions 114 | )); 115 | }; 116 | let Some((_, khr_implicit_shapes_values)) = external_extensions 117 | .iter() 118 | .find(|(n, _)| n.eq_ignore_ascii_case(KHR_IMPLICIT_SHAPES)) 119 | else { 120 | return Err(format!( 121 | "could not find khr_physics_rigid_bodies in extensions: {:#?}", 122 | external_extensions 123 | )); 124 | }; 125 | let khr_implict_shapes = 126 | serde_json::from_value::(khr_implicit_shapes_values.clone()).unwrap(); 127 | 128 | let khr_physics = 129 | serde_json::from_value::(khr_physics_rigid_bodies_values.clone()) 130 | .unwrap(); 131 | 132 | // println!("khr implicit shapes are: {:#?}", khr_implicit_shapes); 133 | // println!("khr implicit shape values of type {:#?}", khr_implicit_shapes_values); 134 | 135 | // let khr_physics_extension_props = serde_json::from_value(khr_physics_rigid_bodies_values.clone()); 136 | 137 | // println!("khr physics is: {:#?}", khr_physics_extension_props); 138 | // println!("khr physics values of type {:#?}", khr_physics_rigid_bodies_values); 139 | 140 | let nodes = gltf.nodes(); 141 | 142 | let mut node_physics = Vec::default(); 143 | 144 | for node in nodes { 145 | let Some(node_properties) = node.extensions() else { 146 | continue; 147 | }; 148 | let Some((key, values)) = node_properties 149 | .iter() 150 | .find(|(n, val)| n.eq_ignore_ascii_case(KHR_PHYSICS_RIGID_BODIES)) 151 | else { 152 | warn!( 153 | "node: {:#?} does not have a rigid_body extension", 154 | node.name() 155 | ); 156 | continue; 157 | // return Err(format!("could not find khr_physics_rigid_bodies in extensions: {:#?}", external_extensions)) 158 | }; 159 | let khr_physics_node_prop = 160 | serde_json::from_value::(values.clone()).unwrap(); 161 | 162 | node_physics.push(( 163 | node.index(), 164 | NodePhysicsMap { 165 | motion: khr_physics_node_prop.motion.mass, 166 | collider_id: khr_physics_node_prop.collider.geometry.shape_index, 167 | }, 168 | )) 169 | // let collider = khr_implict_shapes_extension_props.shapes.iter().nth(khr_physics_node_prop.collider.geometry.shape_index as usize).unwrap(); 170 | 171 | // let collider: MeshPrefab = match collider { 172 | // Shape::Box(box_shape) => Cuboid::from_size(Vec3::from(box_shape.size.size.map(|n| n as f32))).into(), 173 | // Shape::Cylinder(cylinder_shape) => { 174 | // warn!("khr_physics_rigid_body cylinder -> core wrapper conversion not 1:1. These may desync"); 175 | // Cylinder::new(cylinder_shape.dimensions.radius_top as f32, cylinder_shape.dimensions.height as f32).into() 176 | // }, 177 | // }; 178 | // let collider = ColliderFlag::Prefab(collider); 179 | // colliders.push(collider); 180 | } 181 | Ok(( 182 | PhysicsProperties { 183 | implicit_shapes: khr_implict_shapes, 184 | physics_rigid_bodies: khr_physics, 185 | }, 186 | (node_physics), 187 | )) 188 | } 189 | 190 | pub fn bind_physics( 191 | physics: Res, 192 | requests: Query<(Entity, &GltfAssociation, &NodePhysicsRequest, &AssemblyId)>, 193 | nodes: Query<(Entity, &AssemblyId, &NodeId)>, 194 | mut commands: Commands, 195 | ) { 196 | for (e, handle, request, assembly_id) in &requests { 197 | let Some(extension) = physics.0.get(&handle.0) else { 198 | warn!( 199 | "{:#}'s gltf handle, {:#?} does map to a physics extension registry? Skipping.", 200 | e, handle.0 201 | ); 202 | return; 203 | }; 204 | 205 | let implicit_shapes = &extension.implicit_shapes; 206 | let physiscs_rigid_bodies = &extension.physics_rigid_bodies; 207 | 208 | let associated_nodes = nodes 209 | .iter() 210 | .filter(|(e, id, ..)| &assembly_id == id) 211 | .map(|(e, _, n)| (e, n)) 212 | .collect::>(); 213 | 214 | for (node_id, map) in &request.0 { 215 | let Some((e, node_id)) = associated_nodes.iter().find(|(e, n)| &n.0 == node_id) else { 216 | warn!( 217 | "no matching node id found for {:#}. Associated nodes: {:#?}", 218 | node_id, associated_nodes 219 | ); 220 | return; 221 | }; 222 | 223 | let Some(shape) = implicit_shapes.shapes.iter().nth(map.collider_id) else { 224 | warn!( 225 | "no shape found for {:#?}. Shapes are 0 through {:#?}", 226 | map.collider_id, 227 | implicit_shapes.shapes.len() - 1 228 | ); 229 | return; 230 | }; 231 | 232 | let collider: MeshPrefab = match shape { 233 | Shape::Box(box_shape) => { 234 | Cuboid::from_size(Vec3::from(box_shape.size.size.map(|n| n as f32))).into() 235 | } 236 | Shape::Cylinder(cylinder_shape) => { 237 | warn!( 238 | "khr_physics_rigid_body cylinder -> core wrapper conversion not 1:1. These may desync" 239 | ); 240 | Cylinder::new( 241 | cylinder_shape.dimensions.radius_top as f32, 242 | cylinder_shape.dimensions.height as f32, 243 | ) 244 | .into() 245 | } 246 | }; 247 | 248 | commands.entity(*e).insert(ColliderFlag::from(collider)); 249 | } 250 | 251 | commands.entity(e).remove::(); 252 | //let collider = physiscs_rigid_bodies.physics_materials.iter().nth(n) 253 | 254 | //let collider = request.0 255 | // for (id, data) in &request.0 { 256 | 257 | // } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/gltf/synonyms.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::{Handle, RenderAssetUsages}; 2 | use bevy_derive::{Deref, DerefMut}; 3 | use bevy_ecs::prelude::*; 4 | use bevy_gltf::{Gltf, GltfLoaderSettings, GltfMesh, GltfNode, GltfPrimitive}; 5 | use bevy_log::warn; 6 | use bevy_pbr::MeshMaterial3d; 7 | use bevy_render::prelude::*; 8 | use bevy_synonymize_physics::prelude::RequestCollider; 9 | use bytemuck::TransparentWrapper; 10 | use derive_more::derive::From; 11 | use glam::Quat; 12 | use strum::IntoEnumIterator; 13 | 14 | use crate::{ 15 | components::{DisassembleAssetRequest, DisassembleRequest, DisassembleStage, Maybe}, 16 | gltf::{ 17 | GltfAssociation, 18 | physics::{ 19 | GltfPhysicsRegistration, NodePhysicsRequest, PhysicsProperties, parse_gltf_physics, 20 | }, 21 | }, 22 | traits::{AssetLoadSettings, Disassemble, DisassembleSettings, Source, Split, Structure}, 23 | }; 24 | 25 | use super::gltf_collider_request; 26 | 27 | #[derive(Component, Debug)] 28 | pub struct NodeId(pub usize); 29 | 30 | #[derive(From, Clone, Deref, DerefMut, TransparentWrapper)] 31 | #[repr(transparent)] 32 | pub struct GltfPrimitiveWrapper(pub GltfPrimitive); 33 | 34 | impl Disassemble for GltfPrimitiveWrapper { 35 | fn components( 36 | value: &Self, 37 | _settings: DisassembleSettings, 38 | _source: Source, 39 | ) -> Structure { 40 | let mat = value.material.clone().map(|n| MeshMaterial3d(n)); 41 | Structure { 42 | root: ( 43 | Name::new(value.name.clone()), 44 | // Mesh3dAlignmentRequest(value.mesh.clone(), SchemaKind::GLTF), 45 | Mesh3d(value.mesh.clone()), 46 | Maybe(mat), 47 | Visibility::default(), 48 | ), 49 | children: Vec::<()>::default(), 50 | split: Split::default(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Deref, From, TransparentWrapper)] 56 | #[repr(transparent)] 57 | pub struct GltfModel(#[deref] pub Gltf); 58 | 59 | impl AssetLoadSettings for GltfModel { 60 | type LoadSettingsType = GltfLoaderSettings; 61 | 62 | fn load_settings() -> Option { 63 | Some(GltfLoaderSettings { 64 | load_cameras: false, 65 | load_lights: false, 66 | include_source: true, 67 | load_meshes: RenderAssetUsages::default(), 68 | load_materials: RenderAssetUsages::default(), 69 | }) 70 | } 71 | } 72 | 73 | #[derive(From, Clone, Deref, TransparentWrapper)] 74 | #[repr(transparent)] 75 | pub struct GltfNodeWrapper(GltfNode); 76 | 77 | impl AssetLoadSettings for GltfNodeWrapper { 78 | type LoadSettingsType = (); 79 | 80 | fn load_settings() -> Option { 81 | None 82 | } 83 | } 84 | 85 | impl Disassemble for GltfNodeWrapper { 86 | fn components( 87 | value: &Self, 88 | settings: DisassembleSettings, 89 | source: Source, 90 | ) -> Structure { 91 | let mut children = Vec::new(); 92 | 93 | let sub_nodes = &value.0.children; 94 | 95 | let mesh = value.0.mesh.clone().map(|n| { 96 | DisassembleAssetRequest::( 97 | DisassembleStage::Handle(n), 98 | DisassembleSettings::default(), 99 | ) 100 | }); 101 | 102 | for node in sub_nodes { 103 | children.push(DisassembleAssetRequest::::handle( 104 | node.clone(), 105 | None, 106 | )) 107 | } 108 | println!("{:?} node id is {:#?}", value.name.clone(), value.index); 109 | Structure { 110 | root: ( 111 | Name::new(value.name.clone()), 112 | //TransformSchemaAlignRequest(value.transform.clone(), SchemaKind::GLTF), 113 | NodeId(value.index), 114 | value.transform.clone(), 115 | Maybe(mesh), 116 | Visibility::default(), 117 | ), 118 | children: children, 119 | split: Split { 120 | split: false, 121 | inheriet_transform: true, 122 | }, 123 | } 124 | } 125 | } 126 | 127 | impl Disassemble for GltfModel { 128 | fn components( 129 | value: &Self, 130 | settings: DisassembleSettings, 131 | source: Source, 132 | ) -> Structure { 133 | //let nodes = value.nodes; 134 | 135 | println!("scenes: {:#?}", value.scenes); 136 | println!("nodes: {:#?}", value.nodes); 137 | //println!("gltf file: {:#?}", value.source); 138 | 139 | let mut name = "".to_owned(); 140 | 141 | let Source::Asset(gltf_handle) = source else { 142 | panic!("Gltfs are always assets. How did this happen?"); 143 | }; 144 | 145 | let mut physics_extension_maps = PhysicsProperties::default(); 146 | 147 | let gltf_handle = gltf_handle.typed::(); 148 | let mut node_physics_map = Vec::new(); 149 | 150 | let top_node_candidates = &value.nodes; 151 | 152 | let mut top_node = None; 153 | 154 | if let Some(gltf) = &value.source { 155 | (physics_extension_maps, node_physics_map) = parse_gltf_physics(gltf).unwrap(); 156 | 157 | let root_node = gltf.nodes().find(|n| n.children().len() > 0).unwrap(); 158 | name = root_node.name().unwrap_or_default().to_owned(); 159 | 160 | top_node = top_node_candidates.iter().nth(root_node.index()); 161 | } else { 162 | warn!( 163 | "gltf loaded without source. Cannot parse extensions. Offending gltf: {:#?}", 164 | gltf_handle.path() 165 | ) 166 | } 167 | 168 | let top_node = top_node.unwrap(); 169 | 170 | Structure { 171 | root: ( 172 | GltfPhysicsRegistration { 173 | physics: physics_extension_maps, 174 | }, 175 | GltfAssociation(gltf_handle), 176 | NodePhysicsRequest(node_physics_map), 177 | DisassembleAssetRequest::::handle(top_node.clone(), None), 178 | ), 179 | children: Vec::<()>::default(), 180 | split: Split::default(), 181 | } 182 | } 183 | } 184 | 185 | #[derive(From, Clone, Deref, DerefMut, TransparentWrapper)] 186 | #[repr(transparent)] 187 | pub struct GltfMeshWrapper(pub GltfMesh); 188 | 189 | impl AssetLoadSettings for GltfMeshWrapper { 190 | type LoadSettingsType = (); 191 | 192 | fn load_settings() -> Option { 193 | None 194 | } 195 | } 196 | 197 | impl Disassemble for GltfMeshWrapper { 198 | fn components( 199 | value: &Self, 200 | settings: DisassembleSettings, 201 | _source: Source, 202 | ) -> Structure { 203 | let mut children = Vec::new(); 204 | for primitive in &value.0.primitives { 205 | children.push(DisassembleRequest( 206 | GltfPrimitiveWrapper(primitive.clone()), 207 | DisassembleSettings::default(), 208 | )) 209 | } 210 | Structure { 211 | root: (Name::new(value.name.clone()), Visibility::default()), 212 | children: children, 213 | split: Split { 214 | split: settings.split, 215 | inheriet_transform: true, 216 | }, 217 | } 218 | } 219 | } 220 | 221 | /// [`GltfMesh`] that will throw a warning and not initialize if there is more then 1/no primitive 222 | /// 223 | /// Tempory hot-fix for spawning singular primitives. 224 | /// Necessary due to physics with child primitives being unsupported in rapier and avain. 225 | /// https://github.com/bevyengine/bevy/issues/17661 226 | #[derive(From, Deref, Clone, TransparentWrapper)] 227 | #[repr(transparent)] 228 | pub struct GltfPhysicsMeshPrimitive(pub GltfMesh); 229 | 230 | impl AssetLoadSettings for GltfPhysicsMeshPrimitive { 231 | type LoadSettingsType = (); 232 | 233 | fn load_settings() -> Option { 234 | None 235 | } 236 | } 237 | 238 | impl Disassemble for GltfPhysicsMeshPrimitive { 239 | fn components( 240 | value: &Self, 241 | _settings: DisassembleSettings, 242 | _source: Source, 243 | ) -> Structure { 244 | let mesh = { 245 | if value.0.primitives.len() > 1 { 246 | //TODO: maybe replace this with some kind of mesh condenser system? 247 | warn!( 248 | "Multiple primitives found for: {:#}. GltfMeshPrimtiveOne only supports one. Current count: {:#}", 249 | value.0.name, 250 | value.0.primitives.len() 251 | ); 252 | None 253 | } else { 254 | value.0.primitives.first() 255 | } 256 | }; 257 | 258 | let material = mesh 259 | .map(|n| n.material.clone()) 260 | .and_then(|n| n) 261 | .and_then(|n| Some(MeshMaterial3d(n))); 262 | 263 | let primitive = mesh.map(|n| Mesh3d(n.mesh.clone())); 264 | 265 | let collider_request = if let Some(ref collider_kind) = value.0.extras { 266 | RequestCollider::from(gltf_collider_request(collider_kind)) 267 | } else { 268 | RequestCollider::Convex 269 | }; 270 | Structure { 271 | root: (collider_request, Maybe(material), Maybe(primitive)), 272 | children: Vec::<()>::default(), 273 | split: Split::default(), 274 | } 275 | } 276 | } 277 | 278 | #[derive(Clone, Deref, From, TransparentWrapper)] 279 | #[repr(transparent)] 280 | pub struct GltfNodeMeshOne(pub GltfNode); 281 | 282 | impl Disassemble for GltfNodeMeshOne { 283 | fn components( 284 | value: &Self, 285 | _settings: DisassembleSettings, 286 | _source: Source, 287 | ) -> Structure { 288 | let mesh = value.0.mesh.clone().map(|n| { 289 | DisassembleAssetRequest::( 290 | DisassembleStage::Handle(n), 291 | DisassembleSettings::default(), 292 | ) 293 | }); 294 | 295 | let mut adjusted_transform = value.transform; 296 | let gltf_quat = value.transform.rotation; 297 | let adjust_quat = Quat::from_rotation_y(std::f32::consts::PI); 298 | adjusted_transform.rotation = adjust_quat * gltf_quat; 299 | 300 | Structure { 301 | root: ( 302 | Name::new(value.name.clone()), 303 | Maybe(mesh), 304 | //TransformSchemaAlignRequest(value.transform.clone(), SchemaKind::GLTF) 305 | adjusted_transform, 306 | ), 307 | children: Vec::<()>::default(), 308 | split: Split::default(), 309 | } 310 | } 311 | } 312 | 313 | impl AssetLoadSettings for GltfNodeMeshOne { 314 | type LoadSettingsType = (); 315 | 316 | fn load_settings() -> Option { 317 | None 318 | } 319 | } 320 | 321 | /// GltfNode wrapper for spawning gltf nodes with a parent collider mesh, and children visual meshes. 322 | /// This is for physics 323 | #[derive(Clone, Deref, From, TransparentWrapper)] 324 | #[repr(transparent)] 325 | pub struct GltfNodeColliderVisualChilds(pub GltfNode); 326 | 327 | #[derive(Clone, Deref, From, TransparentWrapper)] 328 | #[repr(transparent)] 329 | pub struct GltfNodeVisuals(pub Vec>); 330 | 331 | impl Disassemble for GltfNodeVisuals { 332 | fn components( 333 | value: &Self, 334 | settings: DisassembleSettings, 335 | _source: Source, 336 | ) -> Structure { 337 | let mut children = Vec::new(); 338 | 339 | for handle in value.0.clone() { 340 | children.push(DisassembleAssetRequest::( 341 | DisassembleStage::Handle(handle), 342 | DisassembleSettings::default(), 343 | )) 344 | } 345 | Structure { 346 | root: (), 347 | children: children, 348 | split: Split { 349 | split: settings.split, 350 | inheriet_transform: true, 351 | }, 352 | } 353 | } 354 | } 355 | 356 | impl Disassemble for GltfNodeColliderVisualChilds { 357 | fn components( 358 | value: &Self, 359 | _settings: DisassembleSettings, 360 | _source: Source, 361 | ) -> Structure { 362 | let mesh = value.0.mesh.clone().map(|n| { 363 | DisassembleAssetRequest::( 364 | DisassembleStage::Handle(n), 365 | DisassembleSettings::default(), 366 | ) 367 | }); 368 | 369 | let collider_request = { 370 | if let Some(gltf_extras) = &value.0.extras { 371 | RequestCollider::from(gltf_collider_request(gltf_extras)) 372 | } else { 373 | RequestCollider::Convex 374 | } 375 | }; 376 | Structure { 377 | root: ( 378 | collider_request, 379 | Maybe(mesh), 380 | DisassembleRequest( 381 | GltfNodeVisuals(value.0.children.clone()), 382 | DisassembleSettings::default(), 383 | ), 384 | ), 385 | children: Vec::<()>::default(), 386 | split: Split::default(), 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | use std::collections::{HashMap, HashSet}; 3 | 4 | use bevy_derive::{Deref, DerefMut}; 5 | use bevy_ecs::prelude::ReflectComponent; 6 | use bevy_ecs::prelude::*; 7 | use bevy_reflect::Reflect; 8 | use bevy_synonymize_physics::prelude::JointInfo; 9 | 10 | pub mod components; 11 | pub mod gltf; 12 | pub mod plugins; 13 | pub mod resources; 14 | pub(crate) mod systems; 15 | pub mod traits; 16 | pub mod urdf; 17 | 18 | pub mod prelude { 19 | pub use super::{plugins::*, resources::*, urdf::*}; 20 | } 21 | 22 | /// Id of an assembled structure 23 | #[derive(Component, Reflect, PartialEq, Deref, DerefMut, Clone)] 24 | #[reflect(Component)] 25 | pub struct AssemblyId(pub i64); 26 | 27 | #[derive(Resource, Default, Debug)] 28 | pub struct Assemblies(pub HashMap>); 29 | 30 | #[derive(Event)] 31 | pub struct SaveSuccess { 32 | pub file_name: String, 33 | pub asset_type_id: TypeId, 34 | } 35 | 36 | /// current stage of request for joint from increasing context. 37 | #[derive(Debug, Reflect, Clone)] 38 | pub enum JointRequestStage { 39 | Name(String), 40 | Entity(Entity), 41 | } 42 | 43 | /// Request for a joint. Split into stages depending on available info on joint at time of initialization. Eventually elevated to [`JointFlag`] 44 | #[derive(Component, Debug, Reflect, Clone)] 45 | pub struct JointRequest { 46 | pub stage: JointRequestStage, 47 | pub joint: JointInfo, 48 | } 49 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/plugins.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bevy_app::prelude::*; 4 | use bevy_synonymize::run_proxy_system; 5 | 6 | use crate::{ 7 | Assemblies, AssemblyId, SaveSuccess, 8 | prelude::{AssembleRequests, AssetCheckers, InitializedStagers, RollDownCheckers}, 9 | systems::{ 10 | SaveAssembledRequests, StagedAssembleRequestTasks, align_mesh_to_bevy, 11 | align_transforms_to_bevy, bind_joint_request_to_parent, handle_save_tasks, save_asset, 12 | stage_save_asset_request, 13 | }, 14 | traits::{Assemble, Disassemble}, 15 | urdf::UrdfWrapper, 16 | }; 17 | 18 | /// Plugin for serializing collections of entities/components into a singular asset and vice versa. 19 | pub struct SerializeManyAsOneFor 20 | where 21 | U: 'static + Default + Clone + Assemble + Disassemble, //+ LazyDeserialize, //+ LazySerialize, 22 | { 23 | composed_things_resource: PhantomData U>, 24 | } 25 | 26 | impl Default for SerializeManyAsOneFor 27 | where 28 | U: 'static + Default + Clone + Assemble + Disassemble, 29 | { 30 | fn default() -> Self { 31 | Self { 32 | composed_things_resource: PhantomData, 33 | } 34 | } 35 | } 36 | 37 | impl<'v, T> Plugin for SerializeManyAsOneFor 38 | where 39 | T: 'static + Default + Clone + Assemble + Disassemble, 40 | { 41 | fn build(&self, app: &mut App) { 42 | app 43 | .insert_resource(AssembleRequests::::default()) 44 | .insert_resource(SaveAssembledRequests::::default()) 45 | .add_systems(PreUpdate, stage_save_asset_request::) 46 | // .add_systems(PreUpdate, handle_save_tasks) 47 | .add_systems(PreUpdate, save_asset::) 48 | // .add_systems() 49 | ; 50 | } 51 | } 52 | 53 | pub struct SerializationAssembleBasePlugin; 54 | 55 | impl Plugin for SerializationAssembleBasePlugin { 56 | fn build(&self, app: &mut App) { 57 | app 58 | 59 | .add_plugins(SerializeManyAsOneFor::::default()) 60 | .insert_resource(AssetCheckers::default()) 61 | .insert_resource(InitializedStagers::default()) 62 | .insert_resource(RollDownCheckers::default()) 63 | .insert_resource(Assemblies::default()) 64 | .init_resource::() 65 | .register_type::() 66 | .add_event::() 67 | 68 | // .register_type::() 69 | .add_systems(Update, handle_save_tasks) 70 | .add_systems(Update, run_proxy_system::) 71 | .add_systems(Update, run_proxy_system::) 72 | .add_systems(PreUpdate, bind_joint_request_to_parent) 73 | .add_systems(PreUpdate, align_transforms_to_bevy) 74 | .add_systems(PreUpdate, align_mesh_to_bevy) 75 | //.add_systems(Update, name_from_id) 76 | ; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/resources.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet, VecDeque}, 3 | marker::PhantomData, 4 | }; 5 | 6 | use bevy_asset::prelude::*; 7 | use bevy_derive::{Deref, DerefMut}; 8 | use bevy_ecs::{component::ComponentId, prelude::*, system::SystemId}; 9 | use bevy_transform::prelude::*; 10 | 11 | // /// registry of initialized structures + their children. 12 | // #[derive(Resource, Default)] 13 | // pub struct InitializedChildren(pub HashMap>); 14 | 15 | /// registry of entities that have initialized their staging componenets. 16 | #[derive(Resource, Default)] 17 | pub struct InitializedStagers(pub HashMap>); 18 | 19 | #[derive(Default, Clone)] 20 | pub struct AssembleRequest { 21 | pub file_name: String, 22 | /// path:// keyword path to folder. E.g if a folder is in {ROOT}/assets/models, setting this to `root` will result in root://assets/models 23 | /// being the looked up path. 24 | pub path_keyword: String, 25 | pub selected: HashSet, 26 | _phantom: PhantomData, 27 | } 28 | 29 | /// Processed asset + asset source for that asset to be saved to. 30 | pub struct SaveAssembledRequest { 31 | pub path_keyword: String, 32 | pub asset: T, 33 | pub file_name: String, 34 | } 35 | 36 | impl AssembleRequest { 37 | pub fn new(file_name: String, path_keyword: String, selected: HashSet) -> Self { 38 | Self { 39 | path_keyword, 40 | selected, 41 | file_name, 42 | _phantom: PhantomData::default(), 43 | } 44 | } 45 | } 46 | 47 | #[derive(Default, Clone, Resource, Deref, DerefMut)] 48 | pub struct AssembleRequests(pub Vec>); 49 | 50 | // #[derive(Default, Resource)] 51 | // pub struct SaveAssembledRequests(pub Vec>); 52 | 53 | // /// registry of staging 54 | // pub struct InitializedStagersEntities(pub HashMap>); 55 | 56 | #[derive(Clone)] 57 | pub enum RequestFrom { 58 | ///path of asset relative to main.rs of bevy project. 59 | /// 60 | /// E.G: 61 | /// 62 | ///If `bob.stl` is in `~/project/assets/models/bob.stl`. Then this should be set to `"models/bob.stl"` 63 | AssetServerPath(String), 64 | //AssetId(AssetId), 65 | AssetHandle(Handle), 66 | } 67 | 68 | impl From for RequestFrom { 69 | fn from(value: String) -> Self { 70 | Self::AssetServerPath(value) 71 | } 72 | } 73 | 74 | impl Default for RequestFrom { 75 | fn default() -> Self { 76 | Self::AssetServerPath( 77 | "don't use default for RequestFrom enum or you will get this!".to_owned(), 78 | ) 79 | } 80 | } 81 | 82 | #[derive(Resource, Default, Clone)] 83 | pub struct AssetSpawnRequestQueue { 84 | pub requests: VecDeque>, 85 | } 86 | 87 | #[derive(Resource, Default, Deref)] 88 | pub struct AssetCheckers(pub HashMap); 89 | 90 | /// registry of components to be rolled down onto children. 91 | #[derive(Resource, Default, Deref)] 92 | pub struct RollDownCheckers(pub HashMap); 93 | 94 | /// spawn request for assets that are "all-in-one" rather then composed 95 | /// of seperate components. 96 | /// 97 | /// E.G: Robots/Urdfs are spawned through this. 98 | #[derive(Default, Clone)] 99 | pub struct AssetSpawnRequest { 100 | pub source: RequestFrom, 101 | pub position: Transform, 102 | pub failed_load_attempts: u64, 103 | } 104 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/traits.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::{Asset, AssetLoader, UntypedHandle, meta::Settings, saver::AssetSaver}; 2 | use bevy_ecs::{ 3 | prelude::*, 4 | system::{SystemParam, SystemParamItem}, 5 | }; 6 | use bytemuck::TransparentWrapper; 7 | use std::{any::TypeId, collections::HashSet, ops::Deref}; 8 | 9 | /// The trait for assembling a structure into its root asset. 10 | /// 11 | /// I.E: (Mesh, Material, Name) -> Assemble(FormatWrapper(Format)) -> model.format 12 | pub trait Assemble 13 | where 14 | Self: Sized 15 | + AssembleParms 16 | + Send 17 | + Sync 18 | + 19 | //LazySerialize + 20 | Deref, 21 | { 22 | type Saver: Send 23 | + Default 24 | + AssetSaver 25 | + AssetSaver; 26 | type Loader: Send + Default + AssetLoader; 27 | type Settings: Send + Default; 28 | fn assemble(selected: HashSet, value: SystemParamItem) -> Self::Target; 29 | } 30 | 31 | pub trait AssembleParms { 32 | /// params to fetch world data to assemble(put queries/resource/etc.. like a traditional bevy system in here) 33 | type Params: SystemParam; 34 | } 35 | 36 | #[derive(Clone, Debug, Default)] 37 | pub struct DisassembleSettings { 38 | pub split: bool, 39 | } 40 | 41 | pub enum Source { 42 | Asset(UntypedHandle), 43 | Component, 44 | } 45 | 46 | /// The trait for Disassembling structures into either: 47 | /// 48 | /// A) its sub components 49 | /// 50 | /// B) its children 51 | /// 52 | /// I.E: model.format -> Disassemble(FormatWrapper(Format)) -> (Mesh, Material, Name) 53 | pub trait Disassemble 54 | where 55 | Self: Send + Sync + Deref + TransparentWrapper + 'static, 56 | { 57 | // type Settings: Send + Sync + Clone; 58 | fn components( 59 | value: &Self, 60 | settings: DisassembleSettings, 61 | source: Source, 62 | ) -> Structure; 63 | } 64 | pub trait AssetLoadSettings { 65 | /// Settings for how this asset is loaded 66 | type LoadSettingsType: Settings + Default; 67 | // const LOADSETTINGS: Option<&'static Self::LoadSettingsType>; 68 | fn load_settings() -> Option; 69 | } 70 | 71 | /// Weather to split children off into seperate entities or have them as children to a parent. 72 | #[derive(Debug, Clone, Default)] 73 | pub struct Split { 74 | pub split: bool, 75 | // when spliting, weather to have split parts inheriet transform from their former parent. 76 | // this is nessecary if using transform propagation. 77 | pub inheriet_transform: bool, 78 | } 79 | 80 | #[derive(Clone, Copy, Debug)] 81 | pub struct PullDown(pub TypeId); 82 | 83 | impl PullDown { 84 | pub fn id() -> Self { 85 | Self(TypeId::of::()) 86 | } 87 | } 88 | 89 | // pub enum Structure { 90 | // Root(T), 91 | // Children(Vec, Split), 92 | // } 93 | 94 | pub struct Structure { 95 | /// components going on the entity this component is attached to. 96 | pub root: T, 97 | /// components going onto the sub-components/children of this entity. 98 | pub children: Vec, 99 | /// settings for what (if) children are split off from the parent/child hierarchy. 100 | pub split: Split, 101 | } 102 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/urdf/loader.rs: -------------------------------------------------------------------------------- 1 | //! urdf loarder for robots. Should create a 2 | //! unique urdf resource for models to read from. 3 | 4 | use bevy_app::prelude::*; 5 | use bevy_asset::{ 6 | AssetLoader, AsyncWriteExt, LoadContext, io::Reader, prelude::*, saver::AssetSaver, 7 | }; 8 | use bevy_state::prelude::States; 9 | use thiserror::Error; 10 | 11 | use super::*; 12 | 13 | //contains the machinery required to load urdfs 14 | pub struct UrdfLoaderPlugin; 15 | 16 | impl Plugin for UrdfLoaderPlugin { 17 | fn build(&self, app: &mut App) { 18 | app.init_asset::().init_asset_loader::() 19 | // app.register_asset_saver 20 | ; 21 | } 22 | } 23 | 24 | #[derive(Default)] 25 | pub struct UrdfLoader; 26 | 27 | /// Possible errors that can be produced by [`UrdfLoaderError`] 28 | #[non_exhaustive] 29 | #[derive(Error, Debug)] 30 | pub enum UrdfLoaderError { 31 | #[error("Failed to load Urdf")] 32 | Io(#[from] std::io::Error), 33 | #[error("Failed to parse urdf")] 34 | ParsingError(String), 35 | } 36 | 37 | // impl From for UrdfLoaderError 38 | 39 | impl AssetLoader for UrdfLoader { 40 | type Asset = Urdf; 41 | 42 | type Settings = (); 43 | 44 | type Error = UrdfLoaderError; 45 | 46 | async fn load( 47 | &self, 48 | reader: &mut dyn Reader, 49 | _settings: &(), 50 | _load_context: &mut LoadContext<'_>, 51 | ) -> Result { 52 | let mut bytes = Vec::new(); 53 | reader.read_to_end(&mut bytes).await?; 54 | let urdf = load_urdf(&bytes)?; 55 | Ok(urdf) 56 | } 57 | 58 | fn extensions(&self) -> &[&str] { 59 | &["xml"] 60 | } 61 | } 62 | 63 | pub fn load_urdf<'a>(bytes: &'a [u8]) -> Result { 64 | // if let Some(res) = std::str::from_utf8(bytes) 65 | // .ok() 66 | // .and_then(|utf| urdf_rs::read_from_string(utf).ok()) 67 | // { 68 | // Ok(Urdf { robot: res }) 69 | // } else { 70 | // Err(UrdfLoaderError::ParsingError("")) 71 | // } 72 | let res = std::str::from_utf8(bytes); 73 | match res { 74 | Ok(res) => match urdf_rs::read_from_string(res) { 75 | Ok(urdf) => Ok(Urdf(urdf)), 76 | Err(err) => Err(UrdfLoaderError::ParsingError(err.to_string())), 77 | }, 78 | Err(err) => Err(UrdfLoaderError::ParsingError(err.to_string())), 79 | } 80 | // match std::str::from_utf8(bytes) { 81 | // Ok(_) => todo!(), 82 | // Err(_) => todo!(), 83 | // } 84 | } 85 | 86 | /// Weather this urdf is loaded or not. 87 | #[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)] 88 | pub enum LoadState { 89 | #[default] 90 | Unloaded, 91 | // Loaded, 92 | } 93 | 94 | /// Possible errors that can be produced by [`UrdfLoaderError`] 95 | #[non_exhaustive] 96 | #[derive(Error, Debug)] 97 | pub enum UrdfSaveError { 98 | #[error("Failed to save Urdf")] 99 | Io(#[from] std::io::Error), 100 | #[error("erorr saving urdf")] 101 | UrdfError(#[from] urdf_rs::UrdfError), 102 | } 103 | 104 | #[derive(Default)] 105 | pub struct UrdfSaver; 106 | 107 | #[derive(Default)] 108 | pub struct UrdfSettings; 109 | 110 | impl AssetSaver for UrdfSaver { 111 | type Asset = Urdf; 112 | type Settings = (); 113 | type OutputLoader = UrdfLoader; 114 | type Error = UrdfSaveError; 115 | 116 | fn save( 117 | &self, 118 | writer: &mut bevy_asset::io::Writer, 119 | asset: bevy_asset::saver::SavedAsset<'_, Self::Asset>, 120 | _settings: &Self::Settings, 121 | ) -> impl bevy_tasks::ConditionalSendFuture> { 122 | async move { 123 | let urdf_as_string = urdf_rs::write_to_string(&asset.0)?; 124 | let bytes = urdf_as_string.as_bytes(); 125 | 126 | writer.write_all(bytes).await?; 127 | 128 | Ok(()) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/urdf/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod loader; 2 | pub mod resources; 3 | pub mod urdf; 4 | pub mod visual; 5 | 6 | use crate::traits::AssetLoadSettings; 7 | use bytemuck::TransparentWrapper; 8 | // use crate::systems::split_open_self; 9 | // use crate::systems::split_open_self_children; 10 | use crate::urdf::loader::UrdfLoaderPlugin; 11 | use bevy_app::prelude::*; 12 | use bevy_asset::AssetApp; 13 | use bevy_asset::io::AssetSource; 14 | use bevy_asset::io::file::FileAssetReader; 15 | use bevy_asset::prelude::*; 16 | use bevy_derive::Deref; 17 | use bevy_reflect::TypePath; 18 | use derive_more::derive::From; 19 | use urdf_rs::Robot; 20 | 21 | pub const PACKAGE: &str = "package"; 22 | 23 | #[derive(Asset, TypePath, From, Deref, Debug, Clone, Default, TransparentWrapper)] 24 | #[repr(transparent)] 25 | pub struct UrdfWrapper(pub Urdf); 26 | 27 | impl AssetLoadSettings for UrdfWrapper { 28 | type LoadSettingsType = (); 29 | 30 | fn load_settings() -> Option { 31 | None 32 | } 33 | } 34 | 35 | #[derive(Asset, TypePath, From, Deref, Debug, Clone)] 36 | pub struct Urdf(pub Robot); 37 | 38 | impl Default for Urdf { 39 | fn default() -> Self { 40 | Self(Robot { 41 | name: "DEFAULT_IN_CASE_OF_ERROR".to_owned(), 42 | links: Vec::new(), 43 | joints: Vec::new(), 44 | materials: Vec::new(), 45 | }) 46 | } 47 | } 48 | 49 | /// asset sources for urdf. Needs to be loaded before [`DefaultPlugins`] 50 | pub struct AssetSourcesUrdfPlugin { 51 | // path to folder that `package://`` leads to 52 | pub assets_folder_local_path: String, 53 | } 54 | 55 | impl Plugin for AssetSourcesUrdfPlugin { 56 | fn build(&self, app: &mut App) { 57 | let path = self.assets_folder_local_path.clone(); 58 | app.register_asset_source( 59 | PACKAGE, 60 | AssetSource::build().with_reader(move || Box::new(FileAssetReader::new(path.clone()))), 61 | ); 62 | } 63 | } 64 | 65 | pub struct UrdfSerializationPlugin; 66 | 67 | impl Plugin for UrdfSerializationPlugin { 68 | fn build(&self, app: &mut App) { 69 | app 70 | // .register_type::() 71 | .add_plugins(UrdfLoaderPlugin) 72 | //.add_plugins(SerializeManyAsOneFor::::default()) 73 | //.add_plugins(SerializeManyAsOneFor::::default()) 74 | // .add_systems(Update, split_open_self_children::) 75 | // .add_systems(Update, split_open_self::) 76 | // .add_systems(Update, split_open_self_children::) 77 | //.add_systems(Update, split_open_self::) 78 | ; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/urdf/resources.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::Handle; 2 | use bevy_ecs::prelude::*; 3 | use bevy_reflect::Reflect; 4 | 5 | use super::*; 6 | 7 | #[derive(Resource, Default, Reflect)] 8 | #[reflect(Resource)] 9 | pub struct CachedUrdf { 10 | pub urdf: Handle, 11 | } 12 | -------------------------------------------------------------------------------- /crates/bevy_assemble/src/urdf/visual.rs: -------------------------------------------------------------------------------- 1 | use bevy_derive::Deref; 2 | use bevy_ecs::prelude::*; 3 | use bevy_log::warn; 4 | use bevy_synonymize::prelude::mesh::{Mesh3dFlag, MeshPrefab, MeshWrapper}; 5 | use bytemuck::TransparentWrapper; 6 | use derive_more::From; 7 | use glam::Vec3; 8 | use nalgebra::Vector3; 9 | use urdf_rs::{Geometry, Visual}; 10 | 11 | use bevy_math::prelude::*; 12 | 13 | use crate::{ 14 | components::{DisassembleAssetRequest, DisassembleStage, Resolve}, 15 | gltf::synonyms::GltfPhysicsMeshPrimitive, 16 | traits::{Disassemble, DisassembleSettings, Source, Split, Structure}, 17 | }; 18 | 19 | #[derive(From, Deref, TransparentWrapper)] 20 | #[repr(transparent)] 21 | pub struct VisualWrapper(pub Vec); 22 | 23 | impl Disassemble for VisualWrapper { 24 | fn components( 25 | value: &Self, 26 | settings: DisassembleSettings, 27 | _source: Source, 28 | ) -> Structure { 29 | let mut children = Vec::new(); 30 | for visual in &value.0 { 31 | (children.push(Resolve::from(GeometryWrapper(visual.geometry.clone())))); 32 | } 33 | 34 | Structure { 35 | root: (), 36 | children: children, 37 | split: Split { 38 | split: settings.split, 39 | inheriet_transform: true, 40 | }, 41 | } 42 | } 43 | } 44 | 45 | const FALLBACK_GEOMETRY: Geometry = Geometry::Box { 46 | size: urdf_rs::Vec3([0.1, 0.1, 0.1]), 47 | }; 48 | 49 | #[derive(TransparentWrapper)] 50 | #[repr(transparent)] 51 | pub struct GeometryWrapper(pub Geometry); 52 | 53 | impl From<&Mesh3dFlag> for GeometryWrapper { 54 | fn from(value: &Mesh3dFlag) -> Self { 55 | match value { 56 | Mesh3dFlag::Path(path) => { 57 | let split = path.split("/Primitive").collect::>(); 58 | let mut path = path.to_owned(); 59 | if split.len() > 1 { 60 | warn!( 61 | "until: https://github.com/bevyengine/bevy/issues/17661 is resolved, primitives must be loaded through meshes. Chopping off `Primitive` in mean time. for \n {:#}", 62 | path 63 | ); 64 | path = split 65 | .first() 66 | .map(|n| n.to_string()) 67 | .unwrap_or(path.to_owned()); 68 | } 69 | Self(Geometry::Mesh { 70 | filename: path, 71 | //TODO: check if this is correct 72 | scale: None, 73 | }) 74 | } 75 | Mesh3dFlag::Pure(pure) => match pure { 76 | //TODO: Implement properly. 77 | MeshWrapper::Procedural(_mesh) => { 78 | warn!( 79 | "procedural meshes not supported in urdf serialization(currently) defaulting to error mesh" 80 | ); 81 | Self(FALLBACK_GEOMETRY) 82 | } 83 | MeshWrapper::Prefab(mesh_prefab) => match mesh_prefab { 84 | MeshPrefab::Cuboid(cuboid) => Self(Geometry::Box { 85 | size: urdf_rs::Vec3(cuboid.size().to_array().map(|n| n as f64)), 86 | }), 87 | MeshPrefab::Cylinder(cylinder) => Self(Geometry::Cylinder { 88 | radius: cylinder.radius as f64, 89 | length: (cylinder.half_height * 2.0) as f64, 90 | }), 91 | MeshPrefab::Capsule(capsule3d) => Self(Geometry::Capsule { 92 | radius: capsule3d.radius as f64, 93 | length: (capsule3d.half_length * 2.0) as f64, 94 | }), 95 | MeshPrefab::Sphere(sphere) => Self(Geometry::Sphere { 96 | radius: sphere.radius as f64, 97 | }), 98 | MeshPrefab::Cone(_cone) => { 99 | warn!("Cones not supported by urdf-rs. Using fallback primitive."); 100 | Self(FALLBACK_GEOMETRY) 101 | } 102 | MeshPrefab::Unimplemented(mesh) => { 103 | warn!( 104 | "Unimplemented mesh prefab {:#?}. Using fallback primitive", 105 | mesh 106 | ); 107 | Self(FALLBACK_GEOMETRY) 108 | } 109 | }, 110 | }, 111 | } 112 | } 113 | } 114 | 115 | #[derive(Component)] 116 | pub struct InferModelFormat(pub String); 117 | 118 | impl From 119 | for Resolve> 120 | { 121 | fn from(value: GeometryWrapper) -> Self { 122 | match value.0 { 123 | urdf_rs::Geometry::Box { size } => { 124 | let bevy_size = /*urdf_rotation_flip * */ Vector3::new(size[0], size[1], size[2]); 125 | Resolve::One(Mesh3dFlag::Pure( 126 | MeshPrefab::Cuboid(Cuboid { 127 | half_size: Vec3::new( 128 | bevy_size[0] as f32, 129 | bevy_size[1] as f32, 130 | bevy_size[2] as f32, 131 | ), 132 | }) 133 | .into(), 134 | )) 135 | } 136 | urdf_rs::Geometry::Cylinder { radius, length } => { 137 | //TODO: double check that this is correct 138 | Resolve::One(Mesh3dFlag::Pure( 139 | MeshPrefab::Cylinder(Cylinder { 140 | radius: radius as f32, 141 | half_height: length as f32, 142 | }) 143 | .into(), 144 | )) 145 | } 146 | urdf_rs::Geometry::Capsule { radius, length } => { 147 | //TODO: double check that this is correct 148 | Resolve::One(Mesh3dFlag::Pure( 149 | MeshPrefab::Capsule(Capsule3d { 150 | radius: radius as f32, 151 | half_length: length as f32, 152 | }) 153 | .into(), 154 | )) 155 | } 156 | urdf_rs::Geometry::Sphere { radius } => Resolve::One(Mesh3dFlag::Pure( 157 | MeshPrefab::Sphere(Sphere { 158 | radius: radius as f32, 159 | }) 160 | .into(), 161 | )), 162 | urdf_rs::Geometry::Mesh { filename, .. } => { 163 | Resolve::Other(DisassembleAssetRequest::( 164 | DisassembleStage::Path(filename), 165 | DisassembleSettings::default(), 166 | )) 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_synonymize" 3 | version = "0.8.0-beta.0" 4 | edition = "2024" 5 | repository = "https://github.com/rydb/bevy_serialization_extras" 6 | license = "MIT" 7 | description = "A crate which contains the plugins/systems that bevy_serializations_extras relies on, + some misc synonyms" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | bevy_ecs = {workspace = true} 13 | bevy_render = {workspace = true} 14 | bevy_app = {workspace = true} 15 | bevy_reflect = {workspace = true} 16 | bevy_pbr = {workspace = true} 17 | bevy_utils = {workspace = true} 18 | bevy_math = {workspace = true} 19 | bevy_core_pipeline = {workspace = true} 20 | bevy_asset = {workspace = true} 21 | bevy_transform = {workspace = true} 22 | bevy_color = {workspace = true} 23 | bevy_derive = {workspace = true} 24 | bevy_log = {workspace = true} 25 | glam = {workspace = true} 26 | ron = {workspace = true} 27 | 28 | multimap = {workspace = true} 29 | strum = {workspace = true} 30 | thiserror = {workspace = true} 31 | strum_macros = {workspace = true} 32 | yaserde = {workspace = true} 33 | derive_more = {workspace = true} 34 | log = {workspace = true} 35 | bytemuck = {workspace = true} 36 | 37 | [dev-dependencies] 38 | bevy = {workspace = true, features = ["dynamic_linking"]} 39 | bevy_ui_extras = {workspace = true} 40 | bevy-inspector-egui = {workspace = true} 41 | bevy_window = {workspace = true} 42 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/examples/synonymization.rs: -------------------------------------------------------------------------------- 1 | //! example of correlating a [`bevy_synonyms`] synonym with bevy structs for more functionality. 2 | 3 | 4 | use bevy::{prelude::*, window::PrimaryWindow}; 5 | use bevy_asset::io::{AssetSource, file::FileAssetReader}; 6 | use bevy_inspector_egui::{ 7 | bevy_egui::EguiContext, 8 | egui::{self, TextEdit}, 9 | }; 10 | use bevy_synonymize::{plugins::{SynonymizeBasePlugin}, prelude::material::Material3dFlag}; 11 | use bevy_ui_extras::{UiExtrasDebug, states::DebugMenuState}; 12 | use std::{env, path::PathBuf}; 13 | use strum_macros::{Display, EnumIter}; 14 | const SAVES_LOCATION: &str = "crates/bevy_synonomize/saves"; 15 | 16 | fn main() { 17 | App::new() 18 | .add_plugins(AppSourcesPlugin::CRATE) 19 | .add_plugins(DefaultPlugins.set(WindowPlugin { 20 | exit_condition: bevy::window::ExitCondition::OnPrimaryClosed, 21 | ..Default::default() 22 | })) 23 | .add_plugins(SynonymizeBasePlugin) 24 | .add_plugins(UiExtrasDebug { 25 | menu_mode: DebugMenuState::Explain, 26 | ..default() 27 | }) 28 | .add_systems(Startup, setup) 29 | //TODO: re-add when this has been re-implemented. 30 | //.add_systems(Update, serialization_widgets_ui) 31 | .run(); 32 | } 33 | 34 | /// set up a simple 3D scene 35 | fn setup( 36 | mut commands: Commands, 37 | mut meshes: ResMut>, 38 | mut materials: ResMut>, 39 | asset_server: Res, 40 | ) { 41 | // plane 42 | commands.spawn(( 43 | Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), 44 | MeshMaterial3d(materials.add(Color::srgb(0.4, 0.5, 0.3))), 45 | )); 46 | 47 | let mesh_handle = asset_server.load( 48 | GltfAssetLabel::Primitive { 49 | mesh: 0, 50 | primitive: 0, 51 | } 52 | .from_asset("root://cube.glb"), 53 | ); 54 | 55 | // // cube 56 | commands.spawn(( 57 | Mesh3d(mesh_handle), 58 | Material3dFlag::Pure(Color::Srgba(Srgba::GREEN).into()), 59 | Transform::from_xyz(0.0, 0.5, 0.0), 60 | Name::new("Cube"), 61 | )); 62 | // light 63 | commands.spawn(( 64 | PointLight { 65 | intensity: 1500.0, 66 | shadows_enabled: true, 67 | ..default() 68 | }, 69 | Transform::from_xyz(4.0, 8.0, 4.0), 70 | )); 71 | // camera 72 | commands.spawn(( 73 | Camera3d::default(), 74 | Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 75 | )); 76 | } 77 | 78 | pub const ROOT: &str = "root"; 79 | 80 | /// for filepath asset-loading sanity. 81 | pub enum AppSourcesPlugin { 82 | CRATE, 83 | MAIN, 84 | } 85 | 86 | impl Plugin for AppSourcesPlugin { 87 | fn build(&self, app: &mut App) { 88 | let asset_folder_location = match *self { 89 | Self::CRATE => "../../assets", 90 | Self::MAIN => "assets", 91 | }; 92 | app.register_asset_source( 93 | ROOT, 94 | AssetSource::build() 95 | .with_reader(move || Box::new(FileAssetReader::new(asset_folder_location))), 96 | ); 97 | } 98 | } -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! library for syncing synonomous components with eachother. 2 | 3 | use std::ops::Deref; 4 | 5 | use bevy_ecs::{ 6 | component::ComponentId, 7 | resource::Resource, 8 | system::{Commands, Res, SystemId}, 9 | }; 10 | 11 | // pub mod plugins; 12 | pub mod resources; 13 | mod systems; 14 | pub mod traits; 15 | pub mod plugins; 16 | pub mod synonyms; 17 | 18 | pub mod prelude { 19 | pub use crate::{resources::*, synonyms::*, traits::*}; 20 | } 21 | 22 | #[doc = "hidden"] 23 | pub fn run_proxy_system(proxy_systems: Res, mut commands: Commands) 24 | where 25 | T: Resource + Deref>, 26 | { 27 | for (_, system) in (*proxy_systems).iter() { 28 | commands.run_system(*system); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/plugins.rs: -------------------------------------------------------------------------------- 1 | use std::{any::{type_name, TypeId}, marker::PhantomData}; 2 | 3 | use bevy_app::prelude::*; 4 | use bevy_ecs::prelude::*; 5 | use bevy_asset::prelude::*; 6 | use bevy_log::warn; 7 | use bevy_pbr::StandardMaterial; 8 | use crate::{prelude::{material::{Material3dFlag, MeshMaterial3dRepr}, mesh::Mesh3dFlag, InitializedSynonyms}, traits::{AssetSynonymTarget, SynonymPaths}}; 9 | use crate::{systems::{desynonymize_assset, desynonymize, synonymize, try_synonymize_asset}, traits::{AssetState, ComponentSynonym}}; 10 | 11 | 12 | 13 | /// plugin for converitng between synonymous components. 14 | pub struct SynonymizeComponent { 15 | thing: PhantomData T>, 16 | } 17 | 18 | impl Default for SynonymizeComponent { 19 | fn default() -> Self { 20 | Self { 21 | thing: Default::default(), 22 | } 23 | } 24 | } 25 | 26 | impl Plugin for SynonymizeComponent { 27 | fn build(&self, app: &mut App) { 28 | //TODO: Move this to new crate 29 | //skip_serializing::(app); 30 | app.world_mut(); 31 | // .register_component_hooks::().on_insert(|mut world, e, id| { 32 | // let comp = { 33 | // match world.entity(e).get::() { 34 | // Some(val) => val, 35 | // None => { 36 | // warn!("could not get {:#?} on: {:#}", type_name::(), e); 37 | // return 38 | // }, 39 | // } 40 | // }; 41 | // let target = T::SynonymTarget::from(&comp); 42 | // world.commands().entity(e).insert(target); 43 | // }); 44 | 45 | app.register_type::().add_systems( 46 | PreUpdate, 47 | (synonymize::, desynonymize::).chain(), 48 | ); 49 | } 50 | } 51 | 52 | 53 | /// plugin for converting between synonymous asset component newtypes. 54 | #[derive(Default)] 55 | pub struct SynonymizeAsset { 56 | thing: PhantomData T>, 57 | } 58 | 59 | impl Plugin for SynonymizeAsset { 60 | fn build(&self, app: &mut App) { 61 | //TODO: Move this to new crate 62 | //skip_serializing::(app); 63 | 64 | let synonym_id = TypeId::of::(); 65 | let initializing_repr = type_name::().to_string(); 66 | if let Some(mut initialized_synonyms) = app.world_mut().get_resource_mut::() { 67 | if let Some(first_instance) = initialized_synonyms.get(&synonym_id) { 68 | panic!("multi-initialization found for {:#?}. this is not allowed. First instance {:#}, Second instance {:#}", type_name::(), first_instance, initializing_repr) 69 | } 70 | initialized_synonyms.insert(synonym_id, initializing_repr); 71 | } else { 72 | let mut new_map = InitializedSynonyms::default(); 73 | new_map.insert(synonym_id, initializing_repr); 74 | app.world_mut().insert_resource(new_map); 75 | }; 76 | 77 | app.add_systems( 78 | PreUpdate, 79 | (try_synonymize_asset::, desynonymize_assset::).chain(), 80 | ); 81 | 82 | app.register_type::() 83 | .world_mut() 84 | .register_component_hooks::() 85 | .on_add(|mut world, hook_context| { 86 | let comp = { 87 | match world.entity(hook_context.entity).get::() { 88 | Some(val) => val, 89 | None => { 90 | warn!( 91 | "could not get {:#?} on: {:#}", 92 | type_name::(), 93 | hook_context.entity 94 | ); 95 | return; 96 | } 97 | } 98 | }; 99 | 100 | let handle = { 101 | match comp.asset_state() { 102 | AssetState::Path(path) => { 103 | let Some(asset_server) = world.get_resource::() else { 104 | warn!("could not get asset server?"); 105 | return; 106 | }; 107 | asset_server.load(path) 108 | } 109 | AssetState::Pure(pure) => { 110 | let Some(assets) = world.get_resource::() else { 111 | warn!( 112 | "no mut Assets found for {:#}", 113 | type_name::>() 114 | ); 115 | return; 116 | }; 117 | let asset = T::from_synonym(pure); 118 | assets.add(asset) 119 | } 120 | } 121 | }; 122 | 123 | let componentized_asset = T::Target::from(handle); 124 | world 125 | .commands() 126 | .entity(hook_context.entity) 127 | .insert(componentized_asset); 128 | }); 129 | } 130 | } 131 | 132 | /// base Synonymizations for this library. 133 | pub struct SynonymizeBasePlugin; 134 | 135 | impl Plugin for SynonymizeBasePlugin { 136 | fn build(&self, app: &mut App) { 137 | app 138 | .add_plugins(SynonymizeAsset::>::default()) 139 | ; 140 | } 141 | } -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/resources.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap}; 2 | 3 | use bevy_derive::{Deref, DerefMut}; 4 | use bevy_ecs::prelude::*; 5 | 6 | #[derive(Resource, Default, Deref, DerefMut)] 7 | pub struct InitializedSynonyms{ 8 | synonyms: HashMap 9 | } -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/synonyms/material/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::Asset; 2 | use bevy_color::prelude::*; 3 | use bevy_derive::{Deref, DerefMut}; 4 | use bevy_ecs::component::Component; 5 | use bevy_ecs::prelude::ReflectComponent; 6 | use bevy_pbr::prelude::*; 7 | use bevy_reflect::prelude::*; 8 | use bevy_utils::prelude::*; 9 | use bytemuck::TransparentWrapper; 10 | use derive_more::derive::From; 11 | 12 | use crate::traits::{AssetState, AssetSynonymTarget, SelfPath, SelfPure, SynonymPaths, SynonymPure}; 13 | 14 | /// serializable wrapper for mesh materials 15 | #[derive(Component, Reflect, Clone, PartialEq, From)] 16 | #[reflect(Component)] 17 | pub enum Material3dFlag { 18 | Pure(MaterialWrapper), 19 | Path(String), 20 | } 21 | 22 | impl SynonymPaths for Material3dFlag { 23 | type Pure = MaterialWrapper; 24 | 25 | type Path = String; 26 | 27 | fn asset_state(&self) -> AssetState, SelfPath> { 28 | match self { 29 | Material3dFlag::Pure(material_wrapper) => AssetState::Pure(material_wrapper), 30 | Material3dFlag::Path(path) => AssetState::Path(path), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Clone, From, PartialEq, Reflect)] 36 | pub enum MaterialWrapper { 37 | Color(Color), 38 | } 39 | 40 | #[derive(From, Clone, Deref, DerefMut, Default, TransparentWrapper)] 41 | #[repr(transparent)] 42 | pub struct MeshMaterial3dRepr(MeshMaterial3d); 43 | 44 | impl AssetSynonymTarget for MeshMaterial3dRepr { 45 | type Synonym = Material3dFlag; 46 | type AssetType = StandardMaterial; 47 | 48 | fn from_synonym(value: &SynonymPure) -> Self::AssetType { 49 | match value { 50 | MaterialWrapper::Color(color) => Self::AssetType { 51 | base_color: *color, 52 | ..default() 53 | }, 54 | } 55 | } 56 | 57 | fn from_asset(value: &Self::AssetType) -> SynonymPure { 58 | SynonymPure::::Color(value.base_color) 59 | } 60 | } 61 | 62 | impl Default for Material3dFlag { 63 | fn default() -> Self { 64 | Material3dFlag::Pure(Color::default().into()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/synonyms/mesh/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy_derive::Deref; 2 | use bytemuck::TransparentWrapper; 3 | use derive_more::derive::From; 4 | use crate::traits::*; 5 | 6 | use bevy_ecs::prelude::*; 7 | use bevy_math::prelude::*; 8 | use bevy_reflect::prelude::*; 9 | use bevy_render::prelude::*; 10 | 11 | /// bevy prefab meshes 12 | #[derive(Reflect, Clone, Copy, Debug, PartialEq, From)] 13 | pub enum MeshPrefab { 14 | Cuboid(Cuboid), 15 | Cylinder(Cylinder), 16 | Capsule(Capsule3d), 17 | Sphere(Sphere), 18 | Cone(Cone), 19 | /// Fallback for unimplemented shapes. Should lead to fallback variant mesh. 20 | Unimplemented(&'static str), 21 | } 22 | 23 | impl From<&MeshWrapper> for Mesh { 24 | fn from(value: &MeshWrapper) -> Self { 25 | match value { 26 | MeshWrapper::Prefab(mesh_prefab) => match *mesh_prefab { 27 | MeshPrefab::Cuboid(cuboid) => cuboid.into(), 28 | MeshPrefab::Cylinder(cylinder) => cylinder.into(), 29 | MeshPrefab::Capsule(capsule3d) => capsule3d.into(), 30 | MeshPrefab::Sphere(sphere) => sphere.into(), 31 | MeshPrefab::Cone(cone) => cone.into(), 32 | MeshPrefab::Unimplemented(_kind) => FALLBACK_MESH.into(), 33 | }, 34 | MeshWrapper::Procedural(mesh) => mesh.clone(), 35 | } 36 | } 37 | } 38 | 39 | //TODO: Implement properly when mesh -> mesh file conversion exists to use this. 40 | impl From<&Mesh> for MeshWrapper { 41 | fn from(value: &Mesh) -> Self { 42 | Self::Procedural(value.clone()) 43 | } 44 | } 45 | 46 | impl Default for MeshPrefab { 47 | fn default() -> Self { 48 | Self::Cuboid(Cuboid::from_length(0.1)) 49 | } 50 | } 51 | 52 | #[derive(Component, Reflect, From)] 53 | #[reflect(Component)] 54 | pub enum Mesh3dFlag { 55 | /// asset path to a model from bevy. 56 | //Prefab 57 | // procedural geometry loaded from bevy 58 | //TODO: 59 | Path(String), 60 | //Procedural(ProceduralMeshWrapper), 61 | Pure(MeshWrapper), 62 | } 63 | 64 | impl SynonymPaths for Mesh3dFlag { 65 | type Pure = MeshWrapper; 66 | 67 | type Path = String; 68 | 69 | fn asset_state(&self) -> AssetState, SelfPath> { 70 | match self { 71 | Self::Pure(material_wrapper) => AssetState::Pure(material_wrapper), 72 | Self::Path(path) => AssetState::Path(path), 73 | } 74 | } 75 | } 76 | 77 | #[derive(Reflect, From)] 78 | pub enum MeshWrapper { 79 | Prefab(MeshPrefab), 80 | Procedural(Mesh), 81 | } 82 | 83 | #[derive(TransparentWrapper, Deref)] 84 | #[repr(transparent)] 85 | pub struct Mesh3dRepr(Mesh3d); 86 | 87 | // impl AssetSynonymTarget for Mesh3dRepr { 88 | // type Synonym = Mesh3dFlag; 89 | 90 | // type AssetType = Mesh; 91 | 92 | // fn from_synonym(value: &SynonymPure) -> Self::AssetType { 93 | // match value { 94 | // MeshWrapper::Prefab(mesh_prefab) => mesh_prefab.into(), 95 | // MeshWrapper::Procedural(mesh) => mesh, 96 | // } 97 | // } 98 | 99 | // fn from_asset(value: &Self::AssetType) -> SynonymPure { 100 | // todo!() 101 | // } 102 | // } 103 | 104 | // impl AssetSynonym for Mesh3dFlag { 105 | // type SynonymTarget = Mesh3d; 106 | 107 | // type PureVariant = MeshWrapper; 108 | 109 | // fn asset_state(&self) -> AssetState { 110 | // match self { 111 | // Self::Pure(material_wrapper) => AssetState::Pure(material_wrapper), 112 | // Self::Path(path) => AssetState::Path(path), 113 | // } 114 | // } 115 | // } 116 | 117 | /// TODO: Implement this a bevy <-> mesh converter for this library exists. 118 | /// 119 | #[derive(Reflect, Clone, PartialEq)] 120 | pub(crate) struct ProceduralMeshWrapper; 121 | 122 | impl Default for Mesh3dFlag { 123 | fn default() -> Self { 124 | Self::Pure(MeshWrapper::Procedural(FALLBACK_MESH.into())) 125 | } 126 | } 127 | 128 | pub const FALLBACK_MESH: Cuboid = Cuboid { 129 | half_size: Vec3 { 130 | x: 0.1, 131 | y: 0.1, 132 | z: 0.1, 133 | }, 134 | }; 135 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/synonyms/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod material; 2 | pub mod mesh; -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/systems.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | traits::*, 3 | }; 4 | use std::{ 5 | any::{type_name, TypeId}, 6 | collections::HashMap, ops::Deref, 7 | }; 8 | 9 | use bevy_asset::prelude::*; 10 | use bevy_ecs::prelude::*; 11 | use bevy_log::warn; 12 | // use moonshine_save::save::Save; 13 | 14 | /// synonymizes a component synonym with its target. 15 | pub fn synonymize( 16 | thing_query: Query< 17 | (Entity, &Synonym::SynonymTarget), 18 | Or<( 19 | Added, 20 | Changed, 21 | )>, 22 | >, 23 | synonyms: Query>, 24 | mut commands: Commands, 25 | ) where 26 | Synonym: ComponentSynonym, 27 | { 28 | for (e, f) in thing_query.iter() { 29 | // prevent infinite change back and forth 30 | if synonyms.contains(e) == false { 31 | // entity may not exist when inserting component if entity is deleted in the same frame as this. 32 | // this checks to make sure it exists to prevent a crash. 33 | commands.entity(e).try_insert(Synonym::from(&f)); 34 | } 35 | } 36 | } 37 | 38 | /// takes an asset handle, and spawns a serializable copy of it on its entity 39 | /// try to synonymize a component asset wrapper synonym 40 | pub fn try_synonymize_asset( 41 | assets: ResMut>, 42 | things_query: Query< 43 | (Entity, &SynonymTarget), 44 | Or<( 45 | Changed>, 46 | Added>, 47 | )>, 48 | >, 49 | changed_wrapper: Query>, 50 | mut commands: Commands, 51 | ) where 52 | Impl: AssetSynonymTarget, 53 | { 54 | for (e, thing_handle) in things_query.iter() { 55 | // do not update on the same frame that the target has updated to prevent infinite update chain 56 | if changed_wrapper.contains(e) == false { 57 | let new_wrapper = if let Some(path) = thing_handle.path() { 58 | Impl::Synonym::from(path.to_string()) 59 | } else { 60 | let Some(asset) = assets.get(&**thing_handle) else { 61 | warn!( 62 | "Attempted serialize non-file asset {:#} to {:#} while the asset was unloaded. Skipping attempt", 63 | type_name::(), 64 | type_name::() 65 | ); 66 | return; 67 | }; 68 | let pure = Impl::from_asset(asset); 69 | 70 | Impl::Synonym::from(pure) 71 | }; 72 | 73 | commands.entity(e).try_insert(new_wrapper); 74 | } 75 | } 76 | } 77 | 78 | // /// takes a wrapper component, and deserializes it back into its unserializable asset handle varaint 79 | pub fn desynonymize_assset( 80 | mut assets: ResMut>, 81 | wrapper_thing_query: Query<(Entity, &Impl::Synonym), Changed>, 82 | changed_wrapper_targets: Query>>, 83 | mut commands: Commands, 84 | asset_server: Res, 85 | ) where 86 | Impl: AssetSynonymTarget, 87 | { 88 | for (e, wrapper_thing) in wrapper_thing_query.iter() { 89 | log::trace!("converting wrapper thing {:#?}", e); 90 | 91 | // do not update on the same frame that the target has updated to prevent infinite update chain 92 | if changed_wrapper_targets.contains(e) == false { 93 | let insert = match wrapper_thing.asset_state() { 94 | AssetState::Path(wrapper_path) => { 95 | let handle = asset_server.load(wrapper_path); 96 | SynonymTarget::::from(handle) 97 | } 98 | AssetState::Pure(wrapper) => { 99 | // let new_asset = Impl::AssetType::from(wrapper); 100 | let new_asset = Impl::from_synonym(wrapper); 101 | 102 | let handle = assets.add(new_asset); 103 | SynonymTarget::::from(handle) 104 | } 105 | }; 106 | 107 | commands.entity(e).try_insert(insert); 108 | } 109 | } 110 | } 111 | /// desynonymize a synonym component back into its target. 112 | pub fn desynonymize( 113 | wrapper_thing_query: Query<(Entity, &Synonym), Or<(Added, Changed)>>, 114 | mut commands: Commands, 115 | ) where 116 | Synonym: ComponentSynonym, 117 | { 118 | for (e, f) in wrapper_thing_query.iter() { 119 | commands 120 | .entity(e) 121 | .try_insert(Synonym::SynonymTarget::from(&f)); 122 | } 123 | } 124 | 125 | 126 | -------------------------------------------------------------------------------- /crates/bevy_synonymize/src/traits.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::prelude::*; 2 | use bevy_ecs::prelude::*; 3 | use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, Typed}; 4 | use bytemuck::TransparentWrapper; 5 | use std::ops::Deref; 6 | pub trait ComponentSynonym 7 | where 8 | Self: Component 9 | + Reflect 10 | + FromReflect 11 | + Typed 12 | + GetTypeRegistration 13 | + for<'a> From<&'a Self::SynonymTarget>, 14 | Self::SynonymTarget: Clone + for<'a> From<&'a Self>, 15 | { 16 | type SynonymTarget: Component + Clone; 17 | } 18 | 19 | // pub type AssetType = <::SynonymTarget as AssetHandleComponent>::AssetType; 20 | 21 | // pub type AssetTypeNew = <::Target as Deref>::Target; 22 | pub enum AssetState<'a, Pure, Path: Into> { 23 | Pure(&'a Pure), 24 | Path(&'a Path), 25 | } 26 | 27 | // pub trait AssetSynonym 28 | // where 29 | // Self: Component 30 | // + Reflect 31 | // + FromReflect 32 | // + Typed 33 | // + GetTypeRegistration 34 | // + From 35 | // + From, 36 | // Self::SynonymTarget: Deref>> 37 | // + From>> 38 | // + AssetHandleComponent, 39 | // AssetType: for<'a> From<&'a Self::PureVariant>, 40 | // Self::PureVariant: for<'a> From<&'a AssetType>, 41 | // { 42 | // type PureVariant; 43 | // type SynonymTarget: Component + Deref; 44 | 45 | // fn asset_state(&self) -> AssetState; 46 | // } 47 | 48 | /// convienience alias 49 | pub type SelfPure = ::Pure; 50 | 51 | /// convienience alias 52 | pub type SelfPath = ::Path; 53 | 54 | pub trait SynonymPaths { 55 | type Pure; 56 | type Path; 57 | 58 | fn asset_state(&self) -> AssetState, String>; 59 | } 60 | 61 | pub type SynonymPath = <::Synonym as SynonymPaths>::Path; 62 | pub type SynonymPure = <::Synonym as SynonymPaths>::Pure; 63 | 64 | pub type SynonymTarget = ::Target; 65 | 66 | /// trait for newtype impl around Synonym <-> Target conversions. 67 | pub trait AssetSynonymTarget 68 | where 69 | Self: Deref> + Sized + Component + Deref>> + TransparentWrapper, 70 | Self::Target: Deref, 71 | { 72 | type Synonym: Reflect + FromReflect + GetTypeRegistration + From + From> + Component + SynonymPaths; 73 | type AssetType: Asset; 74 | 75 | fn from_synonym(value: &SynonymPure) -> Self::AssetType; 76 | 77 | fn from_asset(value: &Self::AssetType) -> SynonymPure; 78 | } 79 | 80 | // component on a query that is checked for changes 81 | //FIXME: make this work with a set of components, or better, change to use a "component iter" to have this work for all components in query 82 | pub trait ChangeChecked { 83 | type ChangeCheckedComp: Component; 84 | } -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_synonymize_physics" 3 | version = "0.8.0-beta.0" 4 | edition = "2024" 5 | repository = "https://github.com/rydb/bevy_serialization_extras" 6 | license = "MIT" 7 | description = "A crate for adding physics synonyms for bevy_serialization_extras" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | bevy_synonymize = {workspace = true} 13 | 14 | bevy_ecs = {workspace = true} 15 | bevy_render = {workspace = true} 16 | bevy_app = {workspace = true} 17 | bevy_reflect = {workspace = true} 18 | bevy_pbr = {workspace = true} 19 | bevy_utils = {workspace = true} 20 | bevy_math = {workspace = true} 21 | bevy_core_pipeline = {workspace = true} 22 | bevy_asset = {workspace = true} 23 | bevy_transform = {workspace = true} 24 | bevy_input = {workspace = true} 25 | bevy_rapier3d = {workspace = true} 26 | rapier3d = {workspace = true} 27 | glam = {workspace = true} 28 | # moonshine-save = {workspace = true} 29 | strum = {workspace = true} 30 | strum_macros = {workspace = true} 31 | bitflags = {workspace = true} 32 | derive_more = {workspace = true} 33 | bevy_log = {workspace = true} 34 | 35 | 36 | 37 | 38 | 39 | [dev-dependencies] 40 | bevy = {workspace = true, features = [ 41 | "dynamic_linking", 42 | "trace_tracy" 43 | ]} 44 | bevy-inspector-egui = {workspace = true} 45 | bitvec = {workspace = true} 46 | bevy_ui_extras = {workspace = true} 47 | bevy_window = {workspace = true} 48 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/examples/bevy_rapier_example.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | picking::pointer::{PointerInteraction, PointerPress}, 3 | prelude::*, 4 | }; 5 | use bevy_inspector_egui::{ 6 | bevy_egui::EguiContext, 7 | egui::{self, ScrollArea, TextFormat, Ui, text::LayoutJob}, 8 | }; 9 | use bevy_rapier3d::prelude::*; 10 | use bevy_synonymize::{plugins::SynonymizeBasePlugin, prelude::*}; 11 | use bevy_synonymize_physics::prelude::*; 12 | 13 | use bevy::prelude::Vec3; 14 | use bevy_ui_extras::{UiExtrasDebug, systems::visualize_components_for}; 15 | use bevy_window::PrimaryWindow; 16 | use bitvec::{field::BitField, order::Msb0, view::BitView}; 17 | // use egui::{text::LayoutJob, ScrollArea, TextFormat, Ui}; 18 | use strum::IntoEnumIterator; 19 | use strum_macros::{Display, EnumIter}; 20 | 21 | fn main() { 22 | App::new() 23 | .add_plugins(DefaultPlugins) 24 | .add_plugins(MeshPickingPlugin) 25 | .add_plugins(( 26 | RapierPhysicsPlugin::::default(), 27 | RapierDebugRenderPlugin::default(), 28 | )) 29 | .register_type::() 30 | .register_type::() 31 | .add_plugins(UiExtrasDebug::default()) 32 | .add_plugins(SynonymizeBasePlugin) 33 | .add_plugins(SynonymizePhysicsPlugin) 34 | .register_type::() 35 | .init_resource::() 36 | .init_resource::() 37 | .add_systems( 38 | Update, 39 | visualize_components_for::(bevy_ui_extras::Display::Window), 40 | ) 41 | .add_systems(Update, selection_behaviour) 42 | .add_systems(Startup, setup_graphics) 43 | .add_systems(Startup, create_revolute_joints) 44 | .add_systems(Update, motor_controller_ui) 45 | .add_systems(Update, physics_utilities_ui) 46 | .add_systems(Update, rapier_joint_info_ui) 47 | .run(); 48 | } 49 | 50 | fn setup_graphics(mut commands: Commands) { 51 | commands.spawn(( 52 | Camera3d::default(), 53 | Transform::from_xyz(ORIGIN.x - 5.0, ORIGIN.y, ORIGIN.z) 54 | .looking_at(Vec3::new(13.0, 1.0, 1.0), Vec3::Y), 55 | )); 56 | } 57 | const DASHES: usize = 5; 58 | const CUBE_COUNT: usize = 2; 59 | const ORIGIN: Vec3 = Vec3::new(0.0, 0.0, 0.0); 60 | const NUM: usize = 1; 61 | 62 | #[derive(Component, Reflect)] 63 | pub struct Selectable; 64 | 65 | #[derive(Resource, Default, Clone, Copy, PartialEq)] 66 | pub struct SelectedMotorAxis { 67 | pub axis: MotorAxis, 68 | } 69 | 70 | #[derive(Component, Default, Reflect)] 71 | #[reflect(Component)] 72 | pub struct Selected; 73 | 74 | #[derive(Default, EnumIter, Clone, Copy, Display, PartialEq)] 75 | pub enum MotorAxis { 76 | X = 0, 77 | Y = 1, 78 | Z = 2, 79 | #[default] 80 | ANGX = 3, 81 | ANGY = 4, 82 | ANGZ = 5, 83 | } 84 | #[derive(Default, EnumIter, Display)] 85 | pub enum PhysicsUtilityType { 86 | #[default] 87 | Joints, 88 | } 89 | 90 | #[derive(Resource, Default)] 91 | pub struct PhysicsUtilitySelection { 92 | pub selected: PhysicsUtilityType, 93 | } 94 | 95 | fn create_revolute_joints( 96 | mut commands: Commands, 97 | mut meshes: ResMut>, 98 | mut materials: ResMut>, 99 | ) { 100 | //let rad = 0.4; 101 | let shift = 2.0; 102 | 103 | let mut curr_parent = commands 104 | .spawn(( 105 | RigidBody::Fixed, 106 | Name::new("joint root"), 107 | Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))), 108 | Transform::from_xyz(ORIGIN.x, ORIGIN.y, 0.0), 109 | MeshMaterial3d(materials.add(Color::Srgba(Srgba::BLUE))), 110 | Collider::cuboid(0.25, 0.25, 0.25), 111 | //AsyncCollider::default(), 112 | )) 113 | .id(); 114 | 115 | for i in 0..NUM { 116 | // Create four bodies. 117 | let z = ORIGIN.z + i as f32 * shift * 2.0 + shift; 118 | let positions = [ 119 | Vec3::new(ORIGIN.x, ORIGIN.y, z), 120 | Vec3::new(ORIGIN.x + shift, ORIGIN.y, z), 121 | Vec3::new(ORIGIN.x + shift, ORIGIN.y, z + shift), 122 | Vec3::new(ORIGIN.x, ORIGIN.y, z + shift), 123 | ]; 124 | 125 | let mut handles = [curr_parent; CUBE_COUNT]; 126 | for k in 0..CUBE_COUNT { 127 | handles[k] = commands 128 | .spawn(( 129 | RigidBody::Dynamic, 130 | Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))), 131 | Transform::from_translation(positions[k]), 132 | MeshMaterial3d(materials.add(Color::Srgba(Srgba::BLUE))), 133 | Collider::cuboid(0.25, 0.25, 0.25), 134 | )) 135 | .id(); 136 | } 137 | 138 | // Setup four joints. 139 | let x = Vec3::X; 140 | let z = Vec3::Z; 141 | 142 | let revs = [ 143 | RevoluteJointBuilder::new(z) 144 | .local_anchor2(Vec3::new(0.0, 0.0, -shift)) 145 | .motor_velocity(100.0, 20.0), 146 | RevoluteJointBuilder::new(x).local_anchor2(Vec3::new(-shift, 0.0, 0.0)), 147 | ]; 148 | 149 | commands 150 | .entity(handles[0]) 151 | .insert(ImpulseJoint::new(curr_parent, revs[0])) 152 | .insert(Selectable); 153 | commands 154 | .entity(handles[1]) 155 | .insert(ImpulseJoint::new(handles[0], revs[1])) 156 | .insert(Selectable); 157 | curr_parent = *handles.last().unwrap(); 158 | } 159 | } 160 | 161 | pub fn selection_behaviour( 162 | pointers: Query<(&PointerInteraction, &PointerPress)>, 163 | mut commands: Commands, 164 | selected: Query<&Selected>, 165 | mouse_press: Res>, 166 | ) { 167 | let Ok((hits, press)) = pointers 168 | .get_single() 169 | .inspect_err(|err| warn!("error: {:#}", err)) 170 | else { 171 | return; 172 | }; 173 | 174 | if press.is_primary_pressed() && mouse_press.just_pressed(MouseButton::Left) { 175 | let Some((e, _)) = hits.first() else { 176 | return; 177 | }; 178 | let e = *e; 179 | if selected.contains(e) { 180 | commands.entity(e).remove::(); 181 | } else { 182 | commands.entity(e).insert(Selected); 183 | } 184 | } 185 | } 186 | 187 | pub fn rapier_joint_info_ui( 188 | mut rapier_joint_window: Query<&mut EguiContext, With>, 189 | mut rapier_joints: Query<&ImpulseJoint, With>, 190 | ) { 191 | for mut context in rapier_joint_window.iter_mut() { 192 | egui::Window::new("Rapier Joint Info textbox") 193 | //.frame(DEBUG_FRAME_STYLE) 194 | .show(context.get_mut(), |ui| { 195 | for joint in rapier_joints.iter_mut() { 196 | ScrollArea::vertical() 197 | //.max_height(window_size.unwrap_or(Rect{min: Pos2::default(), max: Pos2::default()}).height()) 198 | .max_height(500.0) 199 | //.id_source(i.to_string() + "_joint") 200 | .show(ui, |ui| { 201 | let joint_as_string = format!("{:#?}", joint); 202 | let job = 203 | LayoutJob::single_section(joint_as_string, TextFormat::default()); 204 | if ui.button("Copy to clipboard").clicked() { 205 | ui.output_mut(|o| o.copied_text = String::from(job.text.clone())); 206 | } 207 | ui.label(job.clone()); 208 | }); 209 | } 210 | }); 211 | } 212 | } 213 | 214 | pub fn motor_controller_ui( 215 | mut selected_joints: Query<(Entity, &mut JointFlag), With>, 216 | keyboard: Res>, 217 | mut contexts: Query<&mut EguiContext, With>, 218 | mut motor_axis: ResMut, 219 | ) { 220 | let negative_accel_key = KeyCode::Minus; 221 | let positive_accel_key = KeyCode::Equal; 222 | 223 | let negative_damping_key = KeyCode::BracketLeft; 224 | let positive_damping_key = KeyCode::BracketRight; 225 | 226 | let motor_index = motor_axis.axis as usize; 227 | 228 | let window_name = "motor controller"; 229 | 230 | //let mut selected_axis = motor_axis.into_inner().clone(); 231 | 232 | for mut context in contexts.iter_mut() { 233 | egui::Window::new(window_name) 234 | //.frame(DEBUG_FRAME_STYLE) 235 | .show(context.get_mut(), |ui| { 236 | ui.label("Controls"); 237 | ui.label("-".repeat(DASHES)); 238 | ui.label(format!( 239 | "positive acceleration: [{:#?}] key", 240 | positive_accel_key 241 | )); 242 | ui.label(format!( 243 | "negative acceleration: [{:#?}] key", 244 | negative_accel_key 245 | )); 246 | ui.label("-".repeat(DASHES)); 247 | 248 | for (e, joint) in selected_joints.iter() { 249 | //ui.label("-".repeat(DASHES)); 250 | ui.label(format!("{:#?}, to {:#?} info", e, joint.parent)); 251 | 252 | ui.label("motor info:"); 253 | //ui.selectable_value(&mut motor_index.index, curent_value, motor_index_text); 254 | 255 | ui.horizontal(|ui| { 256 | for axis in MotorAxis::iter() { 257 | ui.selectable_value(&mut motor_axis.axis, axis, axis.to_string()); 258 | } 259 | }); 260 | ui.label(format!("{:#?}", joint.joint.motors[motor_index])); 261 | // for motor in joint.motors.iter() { 262 | // ui.label(format!("{:#?}",motor)); 263 | // } 264 | ui.label("-".repeat(DASHES)); 265 | } 266 | }); 267 | } 268 | if keyboard.pressed(negative_accel_key) { 269 | for (_, mut joint) in selected_joints.iter_mut() { 270 | joint.joint.motors[motor_index].target_vel += -1.0; 271 | } 272 | } 273 | if keyboard.pressed(positive_accel_key) { 274 | for (_, mut joint) in selected_joints.iter_mut() { 275 | joint.joint.motors[motor_index].target_vel += 1.0; 276 | } 277 | } 278 | 279 | if keyboard.pressed(negative_damping_key) { 280 | for (_, mut joint) in selected_joints.iter_mut() { 281 | joint.joint.motors[motor_index].damping += -1.0; 282 | } 283 | } 284 | if keyboard.pressed(positive_damping_key) { 285 | for (_, mut joint) in selected_joints.iter_mut() { 286 | joint.joint.motors[motor_index].damping += 1.0; 287 | } 288 | } 289 | } 290 | 291 | // A collection of utilities that make debugging physics easier 292 | pub fn physics_utilities_ui( 293 | mut primary_window: Query<&mut EguiContext, With>, 294 | mut utility_selection: ResMut, 295 | 296 | mut selected_joints: Query<(Entity, &mut JointFlag), With>, 297 | ) { 298 | for mut context in primary_window.iter_mut() { 299 | egui::Window::new("Physics Utilities") 300 | //.title_bar(false) 301 | //.frame(DEBUG_FRAME_STYLE) 302 | .show(context.get_mut(), |ui| { 303 | // lay out the ui widget selection menu 304 | ui.horizontal(|ui| { 305 | for utility in PhysicsUtilityType::iter() { 306 | if ui.button(utility.to_string()).clicked() { 307 | utility_selection.selected = utility; 308 | } 309 | } 310 | }); 311 | 312 | match utility_selection.selected { 313 | PhysicsUtilityType::Joints => { 314 | for (e, mut joint) in selected_joints.iter_mut() { 315 | ui.label(format!("{:#?}, to {:#?} info", e, joint.parent)); 316 | ui.label("-".repeat(DASHES)); 317 | //ui.label() 318 | 319 | ui.label("limit axis bits"); 320 | 321 | ui.horizontal(|ui: &mut Ui| { 322 | let mut limit_axis_bits = joint.joint.limit_axes.bits().clone(); 323 | let limit_axis_bitvec = limit_axis_bits.view_bits_mut::(); 324 | 325 | for mut bit in limit_axis_bitvec.iter_mut() { 326 | //let mut bit_value = bit; 327 | 328 | ui.checkbox(&mut bit, ""); 329 | } 330 | let new_joint_mask = JointAxesMaskWrapper::from_bits_truncate( 331 | limit_axis_bitvec.load_le(), 332 | ); 333 | // stops component from being registered as changed if nothing is happening to it 334 | if joint.joint.limit_axes != new_joint_mask { 335 | joint.joint.limit_axes = new_joint_mask; 336 | } 337 | }); 338 | 339 | ui.label("locked axis bits"); 340 | ui.horizontal(|ui| { 341 | let mut locked_axis_bits = joint.joint.locked_axes.bits().clone(); 342 | let limit_axis_bitvec = locked_axis_bits.view_bits_mut::(); 343 | 344 | for mut bit in limit_axis_bitvec.iter_mut() { 345 | //let mut bit_value = bit; 346 | 347 | ui.checkbox(&mut bit, ""); 348 | } 349 | let new_joint_mask = JointAxesMaskWrapper::from_bits_truncate( 350 | limit_axis_bitvec.load_le(), 351 | ); 352 | 353 | if joint.joint.locked_axes != new_joint_mask { 354 | joint.joint.locked_axes = new_joint_mask; 355 | } 356 | }); 357 | ui.label("-".repeat(DASHES)); 358 | } 359 | } 360 | } 361 | }); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate that extends bevy_serialization_extras to include bevy <-> Rapier serialization support. 2 | 3 | pub mod plugins; 4 | mod systems; 5 | pub mod synonyms; 6 | //pub mod loaders; 7 | // pub mod bundles; 8 | 9 | pub mod prelude { 10 | pub use crate::{ 11 | //loaders::*, 12 | // bundles::*, 13 | plugins::*, 14 | synonyms::*, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/plugins.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | prelude::{ 3 | ColliderFlag, collisiongroupfilter::CollisionGroupsFlag, continous_collision::CcdFlag, 4 | friction::FrictionFlag, link::JointRecieverFlag, 5 | }, 6 | systems::{generate_collider_from_children, generate_primitive_for_request}, 7 | synonyms::{ 8 | link::{JointFlag, LinkFlag}, 9 | mass::MassFlag, 10 | rigidbodies::RigidBodyFlag, 11 | solvergroupfilter::SolverGroupsFlag, 12 | }, 13 | }; 14 | 15 | use bevy_app::prelude::*; 16 | use bevy_synonymize::plugins::SynonymizeComponent; 17 | 18 | /// This plugin is an addon for [`SerializationPlugin`] for physics. 19 | pub struct SynonymizePhysicsPlugin; 20 | 21 | impl Plugin for SynonymizePhysicsPlugin { 22 | fn build(&self, app: &mut App) { 23 | app 24 | // .register_type::() 25 | .register_type::() 26 | .register_type::() 27 | .register_type::() 28 | //.register_type::() 29 | .register_type::() 30 | //.add_plugins(SynonymizeComponent::::default()) 31 | .add_plugins(SynonymizeComponent::::default()) 32 | .add_plugins(SynonymizeComponent::::default()) 33 | .add_plugins(SynonymizeComponent::::default()) 34 | .add_plugins(SynonymizeComponent::::default()) 35 | .add_plugins(SynonymizeComponent::::default()) 36 | .add_plugins(SynonymizeComponent::::default()) 37 | //.add_plugins(SerializeQueryFor::::default()) 38 | .add_plugins(SynonymizeComponent::::default()) 39 | .add_systems(Update, generate_primitive_for_request) 40 | .add_systems(Update, generate_collider_from_children); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/colliders.rs: -------------------------------------------------------------------------------- 1 | use bevy_log::warn; 2 | use bevy_math::primitives::{Capsule3d, Cone, Cuboid, Cylinder, Sphere}; 3 | use bevy_rapier3d::prelude::{Collider, ColliderView}; 4 | use bevy_synonymize::{ 5 | prelude::mesh::{FALLBACK_MESH, MeshPrefab}, 6 | traits::ComponentSynonym, 7 | }; 8 | use derive_more::derive::From; 9 | 10 | use bevy_ecs::prelude::*; 11 | use bevy_reflect::prelude::*; 12 | use strum_macros::{Display, EnumIter}; 13 | 14 | /// Component for requesting a collider to be processed from a mesh. 15 | /// If a (non) mesh collider is selected, This will cause collider primitive to be generated to fit around a meshes's geoemtry. 16 | #[derive(EnumIter, Debug, Default, Display, PartialEq, Component, Clone)] 17 | pub enum RequestCollider { 18 | #[default] 19 | Cuboid, 20 | Wheel, 21 | Sphere, 22 | Convex, 23 | } 24 | 25 | #[derive(Component, From)] 26 | pub struct RequestColliderFromChildren(pub RequestCollider); 27 | 28 | use super::{ 29 | collisiongroupfilter::CollisionGroupsFlag, continous_collision::CcdFlag, 30 | solvergroupfilter::SolverGroupsFlag, 31 | }; 32 | 33 | #[derive(Clone, Reflect, From)] 34 | pub struct IgnoredCollider(#[reflect(ignore)] Option, String); 35 | 36 | #[derive(Component, Reflect, Clone, From)] 37 | #[reflect(Component)] 38 | #[require(CcdFlag, CollisionGroupsFlag, SolverGroupsFlag)] 39 | pub enum ColliderFlag { 40 | Prefab(MeshPrefab), 41 | /// ignored variant of collider for unimplemented collider kinds. 42 | Ignore(IgnoredCollider), 43 | } 44 | impl Default for ColliderFlag { 45 | fn default() -> Self { 46 | ColliderFlag::Prefab(MeshPrefab::default()) 47 | } 48 | } 49 | 50 | impl ComponentSynonym for ColliderFlag { 51 | type SynonymTarget = Collider; 52 | } 53 | 54 | impl From<&ColliderFlag> for Collider { 55 | fn from(value: &ColliderFlag) -> Self { 56 | match value { 57 | ColliderFlag::Prefab(mesh_prefab) => { 58 | match mesh_prefab { 59 | MeshPrefab::Cuboid(cuboid) => { 60 | Collider::cuboid(cuboid.half_size.x, cuboid.half_size.y, cuboid.half_size.z) 61 | } 62 | MeshPrefab::Cylinder(cylinder) => { 63 | Collider::cylinder(cylinder.half_height, cylinder.radius) 64 | } 65 | MeshPrefab::Capsule(capsule3d) => { 66 | //TODO: double check that is is correct 67 | Collider::capsule_y(capsule3d.half_length, capsule3d.radius) 68 | } 69 | MeshPrefab::Sphere(sphere) => Collider::ball(sphere.radius), 70 | MeshPrefab::Unimplemented(unimplemented) => { 71 | warn!( 72 | "Attempted to convert unimplemented shape: {:#} to collider. Using fallback instead.", 73 | unimplemented 74 | ); 75 | 76 | // Fallback mesh is a cuboid as the (more accurate) alternative would be performance dropping to 0.1fps from a dozen thosand face trimesh collider. 77 | Collider::cuboid( 78 | FALLBACK_MESH.half_size.x, 79 | FALLBACK_MESH.half_size.z, 80 | FALLBACK_MESH.half_size.z, 81 | ) 82 | } 83 | MeshPrefab::Cone(cone) => Collider::cone(cone.height * 0.5, cone.radius), 84 | } 85 | } 86 | ColliderFlag::Ignore(ignored_collider) => ignored_collider.0.clone().unwrap(), 87 | } 88 | } 89 | } 90 | 91 | impl From<&Collider> for ColliderFlag { 92 | fn from(value: &Collider) -> Self { 93 | let collider = value.as_unscaled_typed_shape(); 94 | //TODO: Implement unimplemented collider types. 95 | match collider { 96 | ColliderView::Ball(ball_view) => Self::Prefab(Sphere::new(ball_view.radius()).into()), 97 | ColliderView::Cuboid(cuboid_view) => { 98 | let (x_half, y_half, z_half) = (cuboid_view.half_extents().x, cuboid_view.half_extents().y, cuboid_view.half_extents().z); 99 | 100 | let x = x_half * 2.0; 101 | let y = y_half * 2.0; 102 | let z = z_half * 2.0; 103 | Self::Prefab(Cuboid::new(x, y, z).into()) 104 | } 105 | ColliderView::Capsule(capsule_view) => { 106 | Self::Prefab(Capsule3d::new(capsule_view.radius(), capsule_view.height()).into()) 107 | } 108 | ColliderView::Segment(view) => { 109 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 110 | } 111 | 112 | ColliderView::Triangle(view) => { 113 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 114 | } 115 | ColliderView::TriMesh(view) => { 116 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 117 | } 118 | ColliderView::Polyline(view) => { 119 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 120 | } 121 | ColliderView::HalfSpace(view) => { 122 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 123 | } 124 | ColliderView::HeightField(view) => { 125 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 126 | } 127 | ColliderView::Compound(view) => { 128 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 129 | } 130 | ColliderView::ConvexPolyhedron(view) => { 131 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 132 | } 133 | ColliderView::Cylinder(cylinder_view) => Self::Prefab( 134 | Cylinder::new(cylinder_view.radius(), cylinder_view.half_height() * 2.0).into(), 135 | ), 136 | ColliderView::Cone(cone_view) => { 137 | Self::Prefab(Cone::new(cone_view.radius(), cone_view.half_height() * 2.0).into()) 138 | } 139 | ColliderView::RoundCuboid(view) => { 140 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 141 | } 142 | ColliderView::RoundTriangle(view) => { 143 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 144 | } 145 | ColliderView::RoundCylinder(view) => { 146 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 147 | } 148 | ColliderView::RoundCone(view) => { 149 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 150 | } 151 | ColliderView::RoundConvexPolyhedron(view) => { 152 | Self::Ignore(((value.clone()).into(), format!("{:#?}", view.raw)).into()) 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/collisiongroupfilter.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::prelude::CollisionGroups; 2 | use bevy_rapier3d::prelude::Group; 3 | 4 | use bevy_ecs::prelude::*; 5 | use bevy_reflect::prelude::*; 6 | use bevy_synonymize::traits::ComponentSynonym; 7 | 8 | #[derive(Component, PartialEq, Reflect, Clone, Default)] 9 | #[reflect(Component)] 10 | pub struct CollisionGroupsFlag { 11 | pub memberships: Group, 12 | pub filters: Group, 13 | } 14 | 15 | impl ComponentSynonym for CollisionGroupsFlag { 16 | type SynonymTarget = CollisionGroups; 17 | } 18 | 19 | impl From<&CollisionGroups> for CollisionGroupsFlag { 20 | fn from(value: &CollisionGroups) -> Self { 21 | Self { 22 | memberships: value.memberships, 23 | filters: value.filters, 24 | } 25 | } 26 | } 27 | 28 | impl From<&CollisionGroupsFlag> for CollisionGroups { 29 | fn from(value: &CollisionGroupsFlag) -> Self { 30 | Self { 31 | memberships: value.memberships, 32 | filters: value.filters, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/continous_collision.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::dynamics::Ccd; 2 | 3 | use bevy_ecs::prelude::*; 4 | use bevy_reflect::prelude::*; 5 | use bevy_synonymize::traits::ComponentSynonym; 6 | 7 | #[derive(Reflect, PartialEq, Component, Clone)] 8 | #[reflect(Component)] 9 | pub struct CcdFlag { 10 | pub enabled: bool, 11 | } 12 | 13 | impl ComponentSynonym for CcdFlag { 14 | type SynonymTarget = Ccd; 15 | } 16 | 17 | impl Default for CcdFlag { 18 | fn default() -> Self { 19 | Self { enabled: true } 20 | } 21 | } 22 | 23 | impl From<&CcdFlag> for Ccd { 24 | fn from(value: &CcdFlag) -> Self { 25 | Self { 26 | enabled: value.enabled, 27 | } 28 | } 29 | } 30 | 31 | impl From<&Ccd> for CcdFlag { 32 | fn from(value: &Ccd) -> Self { 33 | Self { 34 | enabled: value.enabled, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/friction.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::prelude::{CoefficientCombineRule, Friction}; 2 | 3 | use bevy_ecs::prelude::*; 4 | use bevy_reflect::prelude::*; 5 | 6 | #[derive(Reflect, Clone, Default)] 7 | pub enum FrictionCombineRule { 8 | #[default] 9 | Average = 0, 10 | Min, 11 | Multiply, 12 | Max, 13 | } 14 | 15 | #[derive(Component, Reflect, Clone, Default)] 16 | #[reflect(Component)] 17 | pub struct FrictionFlag { 18 | pub friction: f32, 19 | pub friction_combine_rule: FrictionCombineRule, 20 | } 21 | 22 | impl From for FrictionCombineRule { 23 | fn from(value: CoefficientCombineRule) -> Self { 24 | match value { 25 | CoefficientCombineRule::Average => Self::Average, 26 | CoefficientCombineRule::Min => Self::Min, 27 | CoefficientCombineRule::Multiply => Self::Multiply, 28 | CoefficientCombineRule::Max => Self::Max, 29 | } 30 | } 31 | } 32 | 33 | impl From for CoefficientCombineRule { 34 | fn from(value: FrictionCombineRule) -> Self { 35 | match value { 36 | FrictionCombineRule::Average => Self::Average, 37 | FrictionCombineRule::Min => Self::Min, 38 | FrictionCombineRule::Multiply => Self::Multiply, 39 | FrictionCombineRule::Max => Self::Max, 40 | } 41 | } 42 | } 43 | 44 | impl From for Friction { 45 | fn from(value: FrictionFlag) -> Self { 46 | Self { 47 | coefficient: value.friction, 48 | combine_rule: value.friction_combine_rule.into(), 49 | } 50 | } 51 | } 52 | 53 | impl From for FrictionFlag { 54 | fn from(value: Friction) -> Self { 55 | Self { 56 | friction: value.coefficient, 57 | friction_combine_rule: value.combine_rule.into(), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/mass.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::prelude::AdditionalMassProperties; 2 | 3 | use bevy_ecs::prelude::*; 4 | use bevy_reflect::prelude::*; 5 | use bevy_synonymize::traits::ComponentSynonym; 6 | 7 | #[derive(Reflect, PartialEq, Component, Clone)] 8 | pub struct MassFlag { 9 | pub mass: f32, 10 | } 11 | 12 | impl ComponentSynonym for MassFlag { 13 | type SynonymTarget = AdditionalMassProperties; 14 | } 15 | 16 | // W.I.P 17 | impl Default for MassFlag { 18 | fn default() -> Self { 19 | Self { mass: 1.0 } 20 | } 21 | } 22 | 23 | impl From<&MassFlag> for AdditionalMassProperties { 24 | fn from(value: &MassFlag) -> Self { 25 | AdditionalMassProperties::Mass(value.mass) 26 | } 27 | } 28 | 29 | impl From<&AdditionalMassProperties> for MassFlag { 30 | fn from(value: &AdditionalMassProperties) -> Self { 31 | match value { 32 | AdditionalMassProperties::Mass(g) => Self { mass: *g }, 33 | AdditionalMassProperties::MassProperties(mass_properties) => Self { 34 | mass: mass_properties.mass, 35 | }, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod colliders; 2 | pub mod collisiongroupfilter; 3 | pub mod continous_collision; 4 | pub mod friction; 5 | pub mod link; 6 | pub mod mass; 7 | pub mod rigidbodies; 8 | pub mod solvergroupfilter; 9 | 10 | pub use { 11 | colliders::*, collisiongroupfilter::*, continous_collision::*, friction::*, link::*, mass::*, 12 | rigidbodies::*, solvergroupfilter::*, 13 | }; 14 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/rigidbodies.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::prelude::RigidBody; 2 | use bevy_synonymize::traits::ComponentSynonym; 3 | use strum_macros::EnumIter; 4 | 5 | use bevy_ecs::prelude::*; 6 | use bevy_reflect::prelude::*; 7 | 8 | use super::mass::MassFlag; 9 | 10 | #[derive(Component, PartialEq, Reflect, Clone, Default, EnumIter)] 11 | #[reflect(Component)] 12 | #[require(MassFlag)] 13 | pub enum RigidBodyFlag { 14 | #[default] 15 | Fixed, 16 | Dynamic, 17 | } 18 | impl ComponentSynonym for RigidBodyFlag { 19 | type SynonymTarget = RigidBody; 20 | } 21 | 22 | impl From<&RigidBodyFlag> for RigidBody { 23 | fn from(value: &RigidBodyFlag) -> Self { 24 | match value { 25 | RigidBodyFlag::Fixed => Self::Fixed, 26 | RigidBodyFlag::Dynamic => Self::Dynamic, 27 | } 28 | } 29 | } 30 | impl From<&RigidBody> for RigidBodyFlag { 31 | fn from(value: &RigidBody) -> Self { 32 | match *value { 33 | RigidBody::Fixed => Self::Fixed, 34 | RigidBody::Dynamic => Self::Dynamic, 35 | _ => panic!( 36 | "Rigidbody serialization only implemented for fixed and dynamic. populate wrapper for more types" 37 | ), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/synonyms/solvergroupfilter.rs: -------------------------------------------------------------------------------- 1 | use bevy_rapier3d::prelude::Group; 2 | use bevy_rapier3d::prelude::SolverGroups; 3 | 4 | use bevy_ecs::prelude::*; 5 | use bevy_reflect::prelude::*; 6 | use bevy_synonymize::traits::ComponentSynonym; 7 | 8 | pub const PHYSICS_FIXED: SolverGroupsFlag = SolverGroupsFlag { 9 | memberships: GroupWrapper::ALL, 10 | filters: GroupWrapper::ALL, 11 | }; 12 | 13 | /// wrapper around rapier groups to prevent libraries that use this from needing to import the entirety of rapier. 14 | #[derive(Reflect, PartialEq, Clone, Copy)] 15 | pub struct GroupWrapper(pub u32); 16 | 17 | bitflags::bitflags! { 18 | impl GroupWrapper: u32 { 19 | /// The group n°1. 20 | const GROUP_1 = 1 << 0; 21 | /// The group n°2. 22 | const GROUP_2 = 1 << 1; 23 | /// The group n°3. 24 | const GROUP_3 = 1 << 2; 25 | /// The group n°4. 26 | const GROUP_4 = 1 << 3; 27 | /// The group n°5. 28 | const GROUP_5 = 1 << 4; 29 | /// The group n°6. 30 | const GROUP_6 = 1 << 5; 31 | /// The group n°7. 32 | const GROUP_7 = 1 << 6; 33 | /// The group n°8. 34 | const GROUP_8 = 1 << 7; 35 | /// The group n°9. 36 | const GROUP_9 = 1 << 8; 37 | /// The group n°10. 38 | const GROUP_10 = 1 << 9; 39 | /// The group n°11. 40 | const GROUP_11 = 1 << 10; 41 | /// The group n°12. 42 | const GROUP_12 = 1 << 11; 43 | /// The group n°13. 44 | const GROUP_13 = 1 << 12; 45 | /// The group n°14. 46 | const GROUP_14 = 1 << 13; 47 | /// The group n°15. 48 | const GROUP_15 = 1 << 14; 49 | /// The group n°16. 50 | const GROUP_16 = 1 << 15; 51 | /// The group n°17. 52 | const GROUP_17 = 1 << 16; 53 | /// The group n°18. 54 | const GROUP_18 = 1 << 17; 55 | /// The group n°19. 56 | const GROUP_19 = 1 << 18; 57 | /// The group n°20. 58 | const GROUP_20 = 1 << 19; 59 | /// The group n°21. 60 | const GROUP_21 = 1 << 20; 61 | /// The group n°22. 62 | const GROUP_22 = 1 << 21; 63 | /// The group n°23. 64 | const GROUP_23 = 1 << 22; 65 | /// The group n°24. 66 | const GROUP_24 = 1 << 23; 67 | /// The group n°25. 68 | const GROUP_25 = 1 << 24; 69 | /// The group n°26. 70 | const GROUP_26 = 1 << 25; 71 | /// The group n°27. 72 | const GROUP_27 = 1 << 26; 73 | /// The group n°28. 74 | const GROUP_28 = 1 << 27; 75 | /// The group n°29. 76 | const GROUP_29 = 1 << 28; 77 | /// The group n°30. 78 | const GROUP_30 = 1 << 29; 79 | /// The group n°31. 80 | const GROUP_31 = 1 << 30; 81 | /// The group n°32. 82 | const GROUP_32 = 1 << 31; 83 | 84 | /// All of the groups. 85 | const ALL = u32::MAX; 86 | /// None of the groups. 87 | const NONE = 0; 88 | } 89 | } 90 | 91 | impl Default for GroupWrapper { 92 | fn default() -> Self { 93 | GroupWrapper::ALL 94 | } 95 | } 96 | 97 | impl From for Group { 98 | fn from(value: GroupWrapper) -> Self { 99 | Self::from_bits_truncate(value.bits()) 100 | } 101 | } 102 | 103 | impl From for GroupWrapper { 104 | fn from(value: Group) -> Self { 105 | Self::from_bits_truncate(value.bits()) 106 | } 107 | } 108 | 109 | #[derive(Component, PartialEq, Reflect, Clone, Default)] 110 | #[reflect(Component)] 111 | pub struct SolverGroupsFlag { 112 | pub memberships: GroupWrapper, 113 | pub filters: GroupWrapper, 114 | } 115 | 116 | impl ComponentSynonym for SolverGroupsFlag { 117 | type SynonymTarget = SolverGroups; 118 | } 119 | 120 | impl From<&SolverGroupsFlag> for SolverGroups { 121 | fn from(value: &SolverGroupsFlag) -> Self { 122 | Self { 123 | memberships: value.memberships.into(), 124 | filters: value.filters.into(), 125 | } 126 | } 127 | } 128 | 129 | impl From<&SolverGroups> for SolverGroupsFlag { 130 | fn from(value: &SolverGroups) -> Self { 131 | Self { 132 | memberships: value.memberships.into(), 133 | filters: value.memberships.into(), 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_physics/src/systems.rs: -------------------------------------------------------------------------------- 1 | use bevy_asset::Assets; 2 | use bevy_ecs::prelude::*; 3 | use bevy_log::warn; 4 | use bevy_math::primitives::{Cuboid, Sphere}; 5 | use bevy_rapier3d::prelude::{AsyncCollider, ComputedColliderShape}; 6 | use bevy_render::prelude::*; 7 | use bevy_synonymize::prelude::mesh::MeshPrefab; 8 | use glam::Vec3; 9 | use rapier3d::parry::either::Either; 10 | 11 | use crate::prelude::{ColliderFlag, RequestCollider, RequestColliderFromChildren}; 12 | 13 | #[derive(Clone, Copy)] 14 | pub struct FarthestPoints { 15 | pub positive: Vec3, 16 | pub negative: Vec3, 17 | } 18 | 19 | pub fn farthest_points(positions: &[[f32; 3]]) -> FarthestPoints { 20 | let mut farthest_x_positive = 0.0; 21 | let mut farthest_x_negative = 0.0; 22 | 23 | let mut farthest_y_positive = 0.0; 24 | let mut farthest_y_negative = 0.0; 25 | 26 | let mut farthest_z_positive = 0.0; 27 | let mut farthest_z_negative = 0.0; 28 | 29 | for position in positions { 30 | let x = position[0]; 31 | let y = position[1]; 32 | let z = position[2]; 33 | if x > farthest_x_positive { 34 | farthest_x_positive = x; 35 | } 36 | if x < farthest_x_negative { 37 | farthest_x_negative = x; 38 | } 39 | 40 | if y > farthest_y_positive { 41 | farthest_y_positive = y; 42 | } 43 | if y < farthest_y_negative { 44 | farthest_y_negative = y; 45 | } 46 | 47 | if z > farthest_z_positive { 48 | farthest_z_positive = z; 49 | } 50 | if z < farthest_z_negative { 51 | farthest_z_negative = z; 52 | } 53 | } 54 | FarthestPoints { 55 | positive: Vec3::new( 56 | farthest_x_positive, 57 | farthest_y_positive, 58 | farthest_z_positive, 59 | ), 60 | negative: Vec3::new( 61 | farthest_x_negative, 62 | farthest_y_negative, 63 | farthest_z_negative, 64 | ), 65 | } 66 | } 67 | 68 | pub fn collider_from_farthest_points( 69 | request_kind: &RequestCollider, 70 | farthest_points: FarthestPoints, 71 | ) -> Either { 72 | match request_kind { 73 | RequestCollider::Cuboid => { 74 | let half_size = Vec3 { 75 | x: (f32::abs(farthest_points.positive.x) + farthest_points.positive.x), 76 | y: (f32::abs(farthest_points.negative.y) + farthest_points.positive.y), 77 | z: (f32::abs(farthest_points.negative.z) + farthest_points.positive.z), 78 | }; 79 | let collider = Cuboid { half_size }; 80 | Either::Left(ColliderFlag::Prefab(MeshPrefab::from(collider))) 81 | } 82 | //TODO: Until: https://github.com/dimforge/rapier/issues/778 is resolved 83 | //This solution uses the sphere method for generating a primitive. 84 | RequestCollider::Wheel => { 85 | let mut largest = 0.0; 86 | for candidate in [ 87 | farthest_points.positive.x, 88 | f32::abs(farthest_points.negative.x), 89 | farthest_points.positive.y, 90 | f32::abs(farthest_points.negative.y), 91 | farthest_points.positive.z, 92 | f32::abs(farthest_points.negative.z), 93 | ] { 94 | if candidate > largest { 95 | largest = candidate; 96 | } 97 | } 98 | Either::Left(ColliderFlag::Prefab(MeshPrefab::Sphere(Sphere::new( 99 | largest, 100 | )))) 101 | } 102 | RequestCollider::Convex => Either::Right(AsyncCollider(ComputedColliderShape::ConvexHull)), 103 | RequestCollider::Sphere => { 104 | let mut largest = 0.0; 105 | for candidate in [ 106 | farthest_points.positive.x, 107 | f32::abs(farthest_points.negative.x), 108 | farthest_points.positive.y, 109 | f32::abs(farthest_points.negative.y), 110 | farthest_points.positive.z, 111 | f32::abs(farthest_points.negative.z), 112 | ] { 113 | if candidate > largest { 114 | largest = candidate; 115 | } 116 | } 117 | Either::Left(ColliderFlag::Prefab(MeshPrefab::Sphere(Sphere::new( 118 | largest, 119 | )))) 120 | } 121 | } 122 | } 123 | 124 | pub fn generate_collider_from_children( 125 | requests: Query<(Entity, &RequestColliderFromChildren, &Children)>, 126 | meshes: Query<&Mesh3d>, 127 | mesh_assets: Res>, 128 | mut commands: Commands, 129 | ) { 130 | let mut points = FarthestPoints { 131 | positive: Vec3::ZERO, 132 | negative: Vec3::ZERO, 133 | }; 134 | 135 | for (e, request, children) in &requests { 136 | for child in children { 137 | let Ok(mesh) = meshes.get(*child) else { 138 | continue; 139 | }; 140 | let Some(mesh) = mesh_assets.get(&mesh.0) else { 141 | // If a mesh is still loading, don't try to approximate the collider until after its loaded! 142 | return; 143 | }; 144 | let Some(positions) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else { 145 | warn!("Expected positions. Skipping {:#}", child); 146 | continue; 147 | }; 148 | let Some(positions) = positions.as_float3() else { 149 | warn!("Expected positions ot be float3. Skipping {:#}", child); 150 | continue; 151 | }; 152 | let farthest_points = farthest_points(positions); 153 | points.positive = points.positive.max(farthest_points.positive); 154 | points.negative = points.negative.max(farthest_points.negative); 155 | } 156 | let collider = collider_from_farthest_points(&request.0, points); 157 | match collider { 158 | Either::Left(n) => commands.entity(e).insert(n), 159 | Either::Right(n) => commands.entity(e).insert(n), 160 | }; 161 | commands.entity(e).remove::(); 162 | } 163 | } 164 | 165 | /// generate a collider primitive from a primitive request 166 | pub fn generate_primitive_for_request( 167 | requests: Query<(Entity, &RequestCollider, &Mesh3d)>, 168 | mut commands: Commands, 169 | meshes: ResMut>, 170 | ) { 171 | for (e, collider, mesh) in requests.iter() { 172 | let Some(mesh) = meshes.get(&mesh.0) else { 173 | return; 174 | }; 175 | let Some(positions) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else { 176 | warn!("Expected positions. Exiting"); 177 | return; 178 | }; 179 | let Some(positions) = positions.as_float3() else { 180 | warn!("Expected positions ot be float3. Exiting"); 181 | return; 182 | }; 183 | 184 | let farthest_points = farthest_points(positions); 185 | 186 | let collider = collider_from_farthest_points(collider, farthest_points); 187 | match collider { 188 | Either::Left(n) => commands.entity(e).insert(n), 189 | Either::Right(n) => commands.entity(e).insert(n), 190 | }; 191 | commands.entity(e).remove::(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_synonymize_save" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | moonshine-save = {workspace = true} 8 | log = {workspace = true} 9 | bevy_app = {workspace = true} 10 | bevy_ecs = {workspace = true} 11 | bevy_math = {workspace = true} 12 | bevy_reflect = {workspace = true} 13 | bevy_render = {workspace = true} 14 | bevy_core_pipeline = {workspace = true} 15 | bevy_asset = {workspace = true} 16 | bevy_pbr = {workspace = true} 17 | bevy_derive = {workspace = true} 18 | 19 | bevy_synonymize = {workspace = true} 20 | 21 | [dev-dependencies] 22 | bevy = {workspace = true, features = ["dynamic_linking"]} 23 | bevy_ui_extras = {workspace = true} 24 | bevy-inspector-egui = {workspace = true} 25 | bevy_window = {workspace = true} 26 | strum_macros = {workspace = true} 27 | strum = {workspace = true} 28 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/examples/save_load.rs: -------------------------------------------------------------------------------- 1 | //! This is a demo showcasing save/load functionality of bevy_synonymize. 2 | 3 | use bevy::{prelude::*, window::PrimaryWindow}; 4 | use bevy_asset::io::{AssetSource, file::FileAssetReader}; 5 | use bevy_inspector_egui::{ 6 | bevy_egui::EguiContext, 7 | egui::{self, TextEdit}, 8 | }; 9 | use bevy_synonymize::{plugins::SynonymizeBasePlugin, prelude::material::Material3dFlag}; 10 | use bevy_synonymize_save::{plugins::{SerializationPlugin}, resources::{LoadRequest, SaveRequest}}; 11 | use bevy_ui_extras::{UiExtrasDebug, states::DebugMenuState}; 12 | use moonshine_save::save::Save; 13 | use std::{env, path::PathBuf}; 14 | use strum_macros::{Display, EnumIter}; 15 | const SAVES_LOCATION: &str = "crates/bevy_synonymize_save/saves"; 16 | 17 | fn main() { 18 | App::new() 19 | .add_plugins(AppSourcesPlugin::CRATE) 20 | .insert_resource(SetSaveFile { 21 | name: "red".to_owned(), 22 | }) 23 | .add_plugins(DefaultPlugins.set(WindowPlugin { 24 | exit_condition: bevy::window::ExitCondition::OnPrimaryClosed, 25 | ..Default::default() 26 | })) 27 | .insert_resource(UtilitySelection::default()) 28 | .add_plugins(SerializationPlugin) 29 | .add_plugins(SynonymizeBasePlugin) 30 | .add_plugins(UiExtrasDebug { 31 | menu_mode: DebugMenuState::Explain, 32 | ..default() 33 | }) 34 | .add_systems(Startup, setup) 35 | .add_systems(Update, save_file_selection) 36 | //TODO: re-add when this has been re-implemented. 37 | //.add_systems(Update, serialization_widgets_ui) 38 | .run(); 39 | } 40 | 41 | /// set up a simple 3D scene 42 | fn setup( 43 | mut commands: Commands, 44 | mut meshes: ResMut>, 45 | mut materials: ResMut>, 46 | asset_server: Res, 47 | ) { 48 | // plane 49 | commands.spawn(( 50 | Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), 51 | MeshMaterial3d(materials.add(Color::srgb(0.4, 0.5, 0.3))), 52 | )); 53 | 54 | let mesh_handle = asset_server.load( 55 | GltfAssetLabel::Primitive { 56 | mesh: 0, 57 | primitive: 0, 58 | } 59 | .from_asset("root://cube.glb"), 60 | ); 61 | 62 | // // cube 63 | commands.spawn(( 64 | Mesh3d(mesh_handle), 65 | Material3dFlag::Pure(Color::Srgba(Srgba::GREEN).into()), 66 | Transform::from_xyz(0.0, 0.5, 0.0), 67 | Save, 68 | Name::new("Cube"), 69 | )); 70 | // light 71 | commands.spawn(( 72 | PointLight { 73 | intensity: 1500.0, 74 | shadows_enabled: true, 75 | ..default() 76 | }, 77 | Transform::from_xyz(4.0, 8.0, 4.0), 78 | Save, 79 | )); 80 | // camera 81 | commands.spawn(( 82 | Camera3d::default(), 83 | Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 84 | Save, 85 | )); 86 | } 87 | 88 | #[derive(Resource, Default)] 89 | pub struct SetSaveFile { 90 | pub name: String, 91 | } 92 | 93 | pub const ROOT: &str = "root"; 94 | 95 | /// for filepath asset-loading sanity. 96 | pub enum AppSourcesPlugin { 97 | CRATE, 98 | MAIN, 99 | } 100 | 101 | impl Plugin for AppSourcesPlugin { 102 | fn build(&self, app: &mut App) { 103 | let asset_folder_location = match *self { 104 | Self::CRATE => "../../assets", 105 | Self::MAIN => "assets", 106 | }; 107 | app.register_asset_source( 108 | ROOT, 109 | AssetSource::build() 110 | .with_reader(move || Box::new(FileAssetReader::new(asset_folder_location))), 111 | ); 112 | } 113 | } 114 | 115 | pub fn save_file_selection( 116 | mut primary_window_query: Query<&mut EguiContext, With>, 117 | mut save_file_textbox: ResMut, 118 | mut commands: Commands, 119 | ) { 120 | for mut context in primary_window_query.iter_mut() { 121 | let menu_name = "Select a save to load"; 122 | let mut saves_path = PathBuf::default(); 123 | if let Ok(path_check) = env::current_dir() { 124 | saves_path = path_check; 125 | saves_path.push(SAVES_LOCATION) 126 | } 127 | egui::TopBottomPanel::bottom(menu_name).show(context.get_mut(), |ui| { 128 | ui.group(|ui| { 129 | ui.label("Save File: (push enter to save, leave out .ron)"); 130 | ui.add(TextEdit::singleline(&mut save_file_textbox.name)); 131 | 132 | ui.horizontal(|ui| { 133 | if ui.button("save").clicked() { 134 | commands.insert_resource(SaveRequest { 135 | path: SAVES_LOCATION.to_owned() 136 | + "/" 137 | + &save_file_textbox.name 138 | + ".ron", 139 | }) 140 | } 141 | if ui.button("load").clicked() { 142 | commands.insert_resource(LoadRequest { 143 | path: SAVES_LOCATION.to_owned() 144 | + "/" 145 | + &save_file_textbox.name 146 | + ".ron", 147 | }) 148 | } 149 | }); 150 | }); 151 | 152 | if let Ok(folder) = saves_path.read_dir() { 153 | for file_check in folder { 154 | match file_check { 155 | Ok(file) => { 156 | let file_name = file.file_name().to_str().unwrap().to_owned(); 157 | if ui.button(&file_name).clicked() { 158 | commands.insert_resource(SetSaveFile { 159 | name: file_name.replace(".ron", ""), 160 | }) 161 | } 162 | } 163 | _ => {} 164 | } 165 | } 166 | }; 167 | }); 168 | } 169 | } 170 | 171 | #[derive(Default, EnumIter, Display)] 172 | pub enum UtilityType { 173 | #[default] 174 | SerializableList, 175 | } 176 | #[derive(Resource, Default)] 177 | pub struct UtilitySelection { 178 | pub selected: UtilityType, 179 | } 180 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/saves/green.ron: -------------------------------------------------------------------------------- 1 | ( 2 | resources: {}, 3 | entities: {}, 4 | ) -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/saves/red.ron: -------------------------------------------------------------------------------- 1 | ( 2 | resources: {}, 3 | entities: { 4 | 4294967303: ( 5 | components: { 6 | "bevy_core::name::Name": ( 7 | hash: 17134374621268170533, 8 | name: "Cube", 9 | ), 10 | "bevy_render::primitives::Aabb": ( 11 | center: (0.0, 0.0, 0.0), 12 | half_extents: (1.0, 1.0, 1.0), 13 | ), 14 | "bevy_render::view::visibility::InheritedVisibility": (true), 15 | "bevy_render::view::visibility::ViewVisibility": (true), 16 | "bevy_render::view::visibility::Visibility": Inherited, 17 | "bevy_synonymize::synonyms::material::Material3dFlag": Pure(Color(Srgba(( 18 | red: 1.0, 19 | green: 0.0, 20 | blue: 0.0, 21 | alpha: 1.0, 22 | )))), 23 | "bevy_synonymize::synonyms::mesh::Mesh3dFlag": Path("root://cube.glb#Mesh0/Primitive0"), 24 | "bevy_transform::components::global_transform::GlobalTransform": ((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.0)), 25 | "bevy_transform::components::transform::Transform": ( 26 | translation: (0.0, 0.5, 0.0), 27 | rotation: (0.0, 0.0, 0.0, 1.0), 28 | scale: (1.0, 1.0, 1.0), 29 | ), 30 | }, 31 | ), 32 | 4294967304: ( 33 | components: { 34 | "bevy_pbr::bundle::CubemapVisibleEntities": (), 35 | "bevy_pbr::light::point_light::PointLight": ( 36 | color: LinearRgba(( 37 | red: 1.0, 38 | green: 1.0, 39 | blue: 1.0, 40 | alpha: 1.0, 41 | )), 42 | intensity: 1500.0, 43 | range: 20.0, 44 | radius: 0.0, 45 | shadows_enabled: true, 46 | shadow_depth_bias: 0.08, 47 | shadow_normal_bias: 0.6, 48 | shadow_map_near_z: 0.1, 49 | ), 50 | "bevy_render::primitives::CubemapFrusta": (), 51 | "bevy_render::sync_world::SyncToRenderWorld": (), 52 | "bevy_render::view::visibility::InheritedVisibility": (true), 53 | "bevy_render::view::visibility::ViewVisibility": (true), 54 | "bevy_render::view::visibility::Visibility": Inherited, 55 | "bevy_transform::components::global_transform::GlobalTransform": ((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 4.0, 8.0, 4.0)), 56 | "bevy_transform::components::transform::Transform": ( 57 | translation: (4.0, 8.0, 4.0), 58 | rotation: (0.0, 0.0, 0.0, 1.0), 59 | scale: (1.0, 1.0, 1.0), 60 | ), 61 | }, 62 | ), 63 | 4294967305: ( 64 | components: { 65 | "bevy_core_pipeline::core_3d::camera_3d::Camera3d": ( 66 | depth_load_op: Clear(0.0), 67 | depth_texture_usages: (16), 68 | screen_space_specular_transmission_steps: 1, 69 | screen_space_specular_transmission_quality: Medium, 70 | ), 71 | "bevy_core_pipeline::tonemapping::DebandDither": Enabled, 72 | "bevy_core_pipeline::tonemapping::Tonemapping": TonyMcMapface, 73 | "bevy_pbr::cluster::ClusterConfig": FixedZ( 74 | total: 4096, 75 | z_slices: 24, 76 | z_config: ( 77 | first_slice_depth: 5.0, 78 | far_z_mode: MaxClusterableObjectRange, 79 | ), 80 | dynamic_resizing: true, 81 | ), 82 | "bevy_render::camera::camera::Camera": ( 83 | viewport: None, 84 | order: 0, 85 | is_active: true, 86 | target: Window(Primary), 87 | hdr: false, 88 | msaa_writeback: true, 89 | clear_color: Default, 90 | sub_camera_view: None, 91 | ), 92 | "bevy_render::camera::projection::Projection": Perspective(( 93 | fov: 0.7853982, 94 | aspect_ratio: 1.7777778, 95 | near: 0.1, 96 | far: 1000.0, 97 | )), 98 | "bevy_render::primitives::Frustum": (), 99 | "bevy_render::sync_world::SyncToRenderWorld": (), 100 | "bevy_render::view::ColorGrading": ( 101 | global: ( 102 | exposure: 0.0, 103 | temperature: 0.0, 104 | tint: 0.0, 105 | hue: 0.0, 106 | post_saturation: 1.0, 107 | midtones_range: ( 108 | start: 0.2, 109 | end: 0.7, 110 | ), 111 | ), 112 | shadows: ( 113 | saturation: 1.0, 114 | contrast: 1.0, 115 | gamma: 1.0, 116 | gain: 1.0, 117 | lift: 0.0, 118 | ), 119 | midtones: ( 120 | saturation: 1.0, 121 | contrast: 1.0, 122 | gamma: 1.0, 123 | gain: 1.0, 124 | lift: 0.0, 125 | ), 126 | highlights: ( 127 | saturation: 1.0, 128 | contrast: 1.0, 129 | gamma: 1.0, 130 | gain: 1.0, 131 | lift: 0.0, 132 | ), 133 | ), 134 | "bevy_render::view::Msaa": Sample4, 135 | "bevy_render::view::visibility::InheritedVisibility": (true), 136 | "bevy_render::view::visibility::ViewVisibility": (false), 137 | "bevy_render::view::visibility::Visibility": Inherited, 138 | "bevy_render::view::visibility::VisibleEntities": (), 139 | "bevy_transform::components::global_transform::GlobalTransform": ((0.9284767, 0.0, 0.37139064, 0.15638368, 0.9070254, -0.3909592, -0.33686072, 0.42107594, 0.8421519, -2.0, 2.5, 5.0)), 140 | "bevy_transform::components::transform::Transform": ( 141 | translation: (-2.0, 2.5, 5.0), 142 | rotation: (-0.2117188, -0.18465966, -0.040773313, 0.95886046), 143 | scale: (1.0, 1.0, 1.0), 144 | ), 145 | }, 146 | ), 147 | }, 148 | ) -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod plugins; 2 | mod systems; 3 | pub mod resources; -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/src/plugins.rs: -------------------------------------------------------------------------------- 1 | // use super::resources::*; 2 | // use super::systems::*; 3 | // use crate::prelude::material::Material3dFlag; 4 | // use crate::prelude::mesh::Mesh3dFlag; 5 | // use crate::traits::*; 6 | use bevy_core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality}; 7 | use bevy_render::camera::{CameraMainTextureUsages, CameraRenderGraph}; 8 | use log::warn; 9 | use moonshine_save::file_from_resource; 10 | use moonshine_save::load::LoadPlugin; 11 | use moonshine_save::load::load; 12 | use moonshine_save::save::SaveInput; 13 | use moonshine_save::save::SavePlugin; 14 | use moonshine_save::save::save_with; 15 | use std::any::type_name; 16 | use std::ops::Range; 17 | use std::{any::TypeId, marker::PhantomData}; 18 | 19 | use bevy_app::prelude::*; 20 | use bevy_asset::prelude::*; 21 | use bevy_ecs::prelude::*; 22 | use bevy_math::prelude::*; 23 | use bevy_pbr::prelude::*; 24 | use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; 25 | use bevy_render::prelude::*; 26 | 27 | use crate::resources::ComponentsOnSave; 28 | use crate::resources::LoadRequest; 29 | use crate::resources::RefreshCounter; 30 | use crate::resources::SaveRequest; 31 | use crate::resources::SerializeFilter; 32 | use crate::resources::ShowSerializable; 33 | use crate::resources::ShowUnserializable; 34 | use crate::resources::SynonymAssetDeserializers; 35 | use crate::resources::SynonymAssetSerializers; 36 | use crate::resources::SynonymCompDeserializers; 37 | use crate::resources::SynonymCompSerializers; 38 | use crate::resources::TypeRegistryOnSave; 39 | use crate::systems::update_last_saved_typedata; 40 | 41 | 42 | 43 | // /// plugin for converting a query result of something(s) into a singular component. 44 | // pub struct SerializeQueryFor 45 | // where 46 | // S: 'static + QueryData + ChangeChecked, 47 | // T: 'static 48 | // + Component 49 | // + Debug 50 | // + for<'a, 'b> From<&'b <::ReadOnly as WorldQuery>::Item<'a>>, 51 | // U: 'static + Component + for<'a> From<&'a T> + GetTypeRegistration, 52 | // { 53 | // query: PhantomData S>, 54 | // thing: PhantomData T>, 55 | // wrapper_thing: PhantomData U>, 56 | // } 57 | 58 | // impl Plugin for SerializeQueryFor 59 | // where 60 | // S: 'static + QueryData + ChangeChecked, 61 | // T: 'static 62 | // + Component 63 | // + Debug 64 | // + for<'a, 'b> From<&'b <::ReadOnly as WorldQuery>::Item<'a>>, 65 | // U: 'static + Component + for<'a> From<&'a T> + GetTypeRegistration, 66 | // { 67 | // fn build(&self, app: &mut App) { 68 | // skip_serializing::(app); 69 | 70 | // app.register_type::().add_systems( 71 | // PreUpdate, 72 | // (synonymize::, deserialize_as_one::).chain(), 73 | // ); 74 | // } 75 | // } 76 | 77 | // impl Default for SerializeQueryFor 78 | // where 79 | // S: 'static + QueryData + ChangeChecked, 80 | // T: 'static 81 | // + Component 82 | // + Debug 83 | // + for<'a, 'b> From<&'b <::ReadOnly as WorldQuery>::Item<'a>>, 84 | // U: 'static + Component + for<'a> From<&'a T> + GetTypeRegistration, 85 | // { 86 | // fn default() -> Self { 87 | // Self { 88 | // query: PhantomData, 89 | // thing: PhantomData, 90 | // wrapper_thing: PhantomData, 91 | // } 92 | // } 93 | // } 94 | 95 | /// plugin that adds systems/plugins for serialization. 96 | /// `!!!THINGS THAT NEED TO BE SERIALIZED STILL MUST IMPLEMENT .register_type::() IN ORDER TO BE USED!!!` 97 | pub struct SerializationPlugin; 98 | 99 | impl Plugin for SerializationPlugin { 100 | fn build(&self, app: &mut App) { 101 | app.register_type::<[f32; 3]>() 102 | .register_type::() 103 | .register_type::() 104 | .register_type::() 105 | .register_type::() 106 | .register_type::() 107 | .register_type::<[[f32; 3]; 3]>() 108 | .register_type::<[Vec3; 3]>() 109 | .register_type::() 110 | .register_type::() 111 | .register_type::() 112 | .register_type::() 113 | .register_type::() 114 | .register_type::() 115 | .register_type::() 116 | .register_type::() 117 | .register_type::>() 118 | .register_type_data::, ReflectSerialize>() 119 | .register_type_data::, ReflectDeserialize>() 120 | .init_resource::() 121 | .insert_resource(ShowSerializable::default()) 122 | .insert_resource(ShowUnserializable::default()) 123 | .insert_resource(ComponentsOnSave::default()) 124 | .insert_resource(TypeRegistryOnSave::default()) 125 | .insert_resource(RefreshCounter::default()); 126 | app.add_plugins((SavePlugin, LoadPlugin)) 127 | .add_systems( 128 | PreUpdate, 129 | update_last_saved_typedata.run_if(resource_added::), 130 | ) 131 | .add_systems( 132 | PreUpdate, 133 | update_last_saved_typedata.run_if(resource_added::), 134 | ) 135 | .add_systems( 136 | PreUpdate, 137 | update_last_saved_typedata.run_if(resource_changed::), 138 | ) 139 | .add_systems( 140 | PreUpdate, 141 | save_with(save_filter).into(file_from_resource::()), 142 | ) 143 | .init_resource::() 144 | .init_resource::() 145 | .init_resource::() 146 | .init_resource::() 147 | .add_systems(PreUpdate, load(file_from_resource::())); 148 | } 149 | } 150 | /// save filter for this library. 151 | fn save_filter(f: Res) -> SaveInput { 152 | f.0.clone() 153 | } 154 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/src/resources.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap, path::Path}; 2 | 3 | use bevy_derive::{Deref, DerefMut}; 4 | use bevy_ecs::{component::ComponentId, prelude::*, system::SystemId}; 5 | use bevy_reflect::Reflect; 6 | use bevy_render::{camera::{CameraMainTextureUsages, CameraRenderGraph, Exposure}, mesh::Mesh3d}; 7 | use moonshine_save::{prelude::GetFilePath, save::{EntityFilter, SaveInput}}; 8 | 9 | 10 | 11 | 12 | /// Resource version of moonshine-save's [`SaveFilter`]. 13 | #[derive(Resource, Clone, DerefMut, Deref)] 14 | pub struct SerializeFilter(pub SaveInput); 15 | impl Default for SerializeFilter { 16 | fn default() -> Self { 17 | Self({ 18 | // Due to bevy_scene taking `self` and not `&mut self`, need to initialize this twice. 19 | let mut new_filter = SaveInput::default(); 20 | 21 | new_filter.entities = EntityFilter::Any; 22 | new_filter.components = new_filter 23 | .components 24 | .clone() 25 | .deny::() 26 | .deny::() 27 | .deny::() 28 | .deny::(); 29 | new_filter 30 | }) 31 | } 32 | } 33 | 34 | 35 | #[derive(Resource, Reflect)] 36 | #[reflect(Resource)] 37 | pub struct SaveRequest { 38 | pub path: String, 39 | } 40 | 41 | impl GetFilePath for SaveRequest { 42 | fn path(&self) -> &Path { 43 | self.path.as_ref() 44 | } 45 | } 46 | 47 | #[derive(Resource, Reflect)] 48 | #[reflect(Resource)] 49 | pub struct LoadRequest { 50 | pub path: String, 51 | } 52 | 53 | impl GetFilePath for LoadRequest { 54 | fn path(&self) -> &Path { 55 | self.path.as_ref() 56 | } 57 | } 58 | 59 | /// keeps track of number of times refresh request has been sent. For ui utils. 60 | #[derive(Resource, Default)] 61 | pub struct RefreshCounter { 62 | pub counter: usize, 63 | } 64 | 65 | #[derive(Resource, Default, Deref)] 66 | pub struct SynonymAssetSerializers(pub HashMap); 67 | #[derive(Resource, Default, Deref)] 68 | pub struct SynonymAssetDeserializers(pub HashMap); 69 | 70 | #[derive(Resource, Default, Deref)] 71 | pub struct SynonymCompSerializers(pub HashMap); 72 | 73 | #[derive(Resource, Default, Deref)] 74 | pub struct SynonymCompDeserializers(pub HashMap); 75 | 76 | /// contains the state of the type registry since the last [`SaveRequest`]/refresh. 77 | #[derive(Resource, Default, Reflect)] 78 | #[reflect(Resource)] 79 | pub struct TypeRegistryOnSave { 80 | pub registry: HashMap, 81 | } 82 | 83 | /// contains the components marked to saved since last save/refresh. 84 | #[derive(Resource, Default, Reflect)] 85 | #[reflect(Resource)] 86 | pub struct ComponentsOnSave { 87 | pub components: HashMap, 88 | } 89 | 90 | #[derive(Resource, Reflect)] 91 | #[reflect(Resource)] 92 | pub struct ShowSerializable { 93 | pub check: bool, 94 | } 95 | 96 | impl Default for ShowSerializable { 97 | fn default() -> Self { 98 | Self { check: false } 99 | } 100 | } 101 | 102 | #[derive(Resource, Reflect)] 103 | #[reflect(Resource)] 104 | pub struct ShowUnserializable { 105 | pub check: bool, 106 | } 107 | 108 | impl Default for ShowUnserializable { 109 | fn default() -> Self { 110 | Self { check: true } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/bevy_synonymize_save/src/systems.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, collections::HashMap}; 2 | 3 | use bevy_app::App; 4 | use bevy_ecs::prelude::*; 5 | use bevy_reflect::TypeInfo; 6 | use moonshine_save::save::Save; 7 | 8 | use crate::resources::{ComponentsOnSave, SerializeFilter, TypeRegistryOnSave}; 9 | 10 | 11 | /// adds the given type to the skipped types list when serializing 12 | pub fn skip_serializing(app: &mut App) { 13 | type L = SerializeFilter; 14 | let mut skip_list: Mut<'_, SerializeFilter> = app 15 | .world_mut() 16 | .get_resource_or_insert_with::(|| L::default()); 17 | 18 | let skip_list_copy = skip_list.clone(); 19 | skip_list.0.components = skip_list_copy 20 | .0 21 | .components 22 | .deny_by_id(TypeId::of::()); 23 | } 24 | 25 | pub fn update_last_saved_typedata(world: &mut World) { 26 | let mut enetities_to_save = world.query_filtered::>(); 27 | 28 | log::trace!("updating last saved type_data"); 29 | 30 | let type_registry = world.resource::(); 31 | 32 | let mut saved_component_types = HashMap::new(); 33 | for e in enetities_to_save.iter(&world) { 34 | for component in world.entity(e).archetype().components() { 35 | let comp_info = world.components().get_info(component).unwrap(); 36 | saved_component_types.insert(comp_info.type_id().unwrap(), comp_info.name().to_owned()); 37 | } 38 | } 39 | 40 | let registered_types = type_registry 41 | .read() 42 | .iter() 43 | .map(|id| { 44 | let type_id = id.type_id(); 45 | 46 | return (type_id, TypeInfo::type_path(id.type_info()).to_owned()); 47 | }) 48 | .collect::>(); 49 | 50 | type L = TypeRegistryOnSave; 51 | world.insert_resource::(L { 52 | registry: registered_types, 53 | }); 54 | type O = ComponentsOnSave; 55 | world.insert_resource::(O { 56 | components: saved_component_types, 57 | }); 58 | } -------------------------------------------------------------------------------- /demo_gif.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/demo_gif.webm -------------------------------------------------------------------------------- /diff_bot: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /edit_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rydb/bevy_serialization_extras/8a11b4981a2b2cf264c79a5b67bd2f74adbfbd63/edit_example.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod prelude { 2 | #[cfg(feature = "assemble")] 3 | pub use bevy_assemble::prelude::*; 4 | pub use bevy_synonymize::prelude::*; 5 | #[cfg(feature = "physics")] 6 | pub use bevy_synonymize_physics::prelude::*; 7 | } 8 | 9 | // pub use bevy_synonymize; 10 | // pub use bevy_synonymize_physics; 11 | --------------------------------------------------------------------------------