diff --git a/crates/blenvy/Cargo.toml b/crates/blenvy/Cargo.toml
index 926282e..adb59db 100644
--- a/crates/blenvy/Cargo.toml
+++ b/crates/blenvy/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "blenvy"
-version = "0.0.1"
+version = "0.1.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define Bevy components direclty inside gltf files and instanciate the components on the Bevy side."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
@@ -17,6 +17,8 @@ workspace = true
bevy = { version = "0.14.0-rc.3", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
serde = "1.0.188"
ron = "0.8.1"
+serde_json = "1.0.108"
+
[dev-dependencies]
bevy = { version = "0.14.0-rc.3", default-features = false, features = ["dynamic_linking"] }
\ No newline at end of file
diff --git a/crates/blenvy/LICENSE.md b/crates/blenvy/LICENSE.md
new file mode 100644
index 0000000..ad21aac
--- /dev/null
+++ b/crates/blenvy/LICENSE.md
@@ -0,0 +1,4 @@
+This crate is available under either:
+
+* The [MIT License](./LICENSE_MIT)
+* The [Apache License, Version 2.0](./LICENSE_APACHE)
\ No newline at end of file
diff --git a/crates/blenvy/LICENSE_APACHE.md b/crates/blenvy/LICENSE_APACHE.md
new file mode 100644
index 0000000..f748977
--- /dev/null
+++ b/crates/blenvy/LICENSE_APACHE.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2023] [Mark "kaosat-dev" Moissette]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/crates/blenvy/LICENSE_MIT.md b/crates/blenvy/LICENSE_MIT.md
new file mode 100644
index 0000000..c7e0144
--- /dev/null
+++ b/crates/blenvy/LICENSE_MIT.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Mark "kaosat-dev" Moissette
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/crates/blenvy/README.md b/crates/blenvy/README.md
new file mode 100644
index 0000000..000f05b
--- /dev/null
+++ b/crates/blenvy/README.md
@@ -0,0 +1,333 @@
+[![Crates.io](https://img.shields.io/crates/v/blenvy)](https://crates.io/crates/blenvy)
+[![Docs](https://img.shields.io/docsrs/blenvy)](https://docs.rs/blenvy/latest/blenvy/)
+[![License](https://img.shields.io/crates/l/blenvy)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/blenvy/License.md)
+[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
+
+# blenvy
+
+this crate adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy.
+
+* Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded
+* Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
+
+A blueprint is a set of **overrideable** components + a hierarchy: ie
+
+ * just a Gltf file with Gltf_extras specifying components
+ * a component called BlueprintName
+
+Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-on that do a lot of the work for you
+- [blenvy](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy)
+
+
+## Usage
+
+Here's a minimal usage example:
+
+```toml
+# Cargo.toml
+[dependencies]
+bevy="0.14"
+blenvy = { version = "0.1.0"}
+
+```
+
+```rust no_run
+use bevy::prelude::*;
+use blenvy::*;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .add_plugins(BlenvyPlugin)
+
+ .run();
+}
+
+// not shown here: any other setup that is not specific to blueprints
+
+fn spawn_blueprint(
+ mut commands: Commands,
+ keycode: Res>,
+){
+ if keycode.just_pressed(KeyCode::S) {
+ let new_entity = commands.spawn((
+ BlueprintName("Health_Pickup".to_string()), // mandatory !!
+ SpawnHere, // mandatory !!
+ TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // VERY important !!
+ // any other component you want to insert
+ ));
+ }
+}
+```
+
+## Installation
+
+Add the following to your `[dependencies]` section in `Cargo.toml`:
+
+```toml
+blenvy = "0.1.0"
+```
+
+Or use `cargo add`:
+
+```toml
+cargo add blenvy
+```
+
+## Setup
+
+```rust no_run
+use bevy::prelude::*;
+use blenvy::*;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .add_plugin(BlenvyPlugin)
+
+ .run();
+}
+
+```
+
+you may want to configure your "library"/"blueprints" settings:
+
+```rust no_run
+use bevy::prelude::*;
+use blenvy::*;
+
+fn main() {
+ App::new()
+ .add_plugins((
+ BlenvyPlugin{
+ aabbs: true, // defaults to false, enable this to automatically calculate aabb for the scene/blueprint
+ ..Default::default()
+ }
+ ))
+ .run();
+}
+
+```
+
+## Spawning entities from blueprints
+
+You can spawn entities from blueprints like this:
+```rust no_run
+commands.spawn((
+ BlueprintName("Health_Pickup".to_string()), // mandatory !!
+ SpawnHere, // mandatory !!
+
+ TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // optional
+ // any other component you want to insert
+))
+
+```
+
+Once spawning of the actual entity is done, the spawned Blueprint will be *gone/merged* with the contents of Blueprint !
+
+> Important :
+you can **add** or **override** components present inside your Blueprint when spawning the BluePrint itself: ie
+
+### Adding components not specified inside the blueprint
+
+you can just add any additional components you need when spawning :
+
+```rust no_run
+commands.spawn((
+ BlueprintName("Health_Pickup".to_string()),
+ SpawnHere,
+ TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
+ // from Rapier/bevy_xpbd: this means the entity will also have a velocity component when inserted into the world
+ Velocity {
+ linvel: Vec3::new(vel_x, vel_y, vel_z),
+ angvel: Vec3::new(0.0, 0.0, 0.0),
+ },
+))
+
+```
+### Overriding components specified inside the blueprint
+
+any component you specify when spawning the Blueprint that is also specified **within** the Blueprint will **override** that component in the final spawned entity
+
+for example
+```rust no_run
+commands.spawn((
+ BlueprintName("Health_Pickup".to_string()),
+ SpawnHere,
+ TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
+ HealthPowerUp(20)// if this is component is also present inside the "Health_Pickup" blueprint, that one will be replaced with this component during spawning
+))
+
+```
+
+### BluePrintBundle
+
+There is also a ```BluePrintBundle``` for convenience , which just has
+ * a ```BlueprintName``` component
+ * a ```SpawnHere``` component
+
+## Additional information
+
+- When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert
+- In cases where that is undesirable, you can add a ```NoInBlueprint``` component on the entity you spawn the blueprint with, and the components above will not be add
+- if you want to overwrite the **path** where this crate looks for blueprints (gltf files) , you can add a ```Library``` component , and that will be used instead of the default path
+ie :
+
+```rust no_run
+commands
+ .spawn((
+ Name::from("test"),
+ BluePrintBundle {
+ blueprint: BlueprintName("TestBlueprint".to_string()),
+ ..Default::default()
+ },
+ Library("models".into()) // now the path to the blueprint above will be /assets/models/TestBlueprint.glb
+ ))
+```
+- this crate also provides a special optional ```GameWorldTag``` component: this is useful when you want to keep all your spawned entities inside a root entity
+
+You can use it in your queries to add your entities as children of this "world"
+This way all your levels, your dynamic entities etc, are kept seperated from UI nodes & other entities that are not relevant to the game world
+
+> Note: you should only have a SINGLE entity tagged with that component !
+
+```rust no_run
+ commands.spawn((
+ SceneBundle {
+ scene: models
+ .get(game_assets.world.id())
+ .expect("main level should have been loaded")
+ .scenes[0]
+ .clone(),
+ ..default()
+ },
+ bevy::prelude::Name::from("world"),
+ GameWorldTag, // here it is
+ ));
+```
+
+
+## SystemSet
+
+the ordering of systems is very important !
+
+For example to replace your proxy components (stand-in components when you cannot/ do not want to use real components in the gltf file) with actual ones, which should happen **AFTER** the Blueprint based spawning,
+
+so ```blenvy``` provides a **SystemSet** for that purpose: ```GltfBlueprintsSet```
+
+Typically , the order of systems should be
+
+***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***blenvy (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)*** => ***replace_proxies***
+
+see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic for how to set it up correctly
+
+
+
+## Animation
+
+```blenvy``` provides some lightweight helpers to deal with animations stored in gltf files
+
+ * an ```Animations``` component that gets inserted into spawned (root) entities that contains a hashmap of all animations contained inside that entity/gltf file .
+ (this is a copy of the ```named_animations``` inside Bevy's gltf structures )
+ * an ```AnimationPlayerLink``` component that gets inserted into spawned (root) entities, to make it easier to trigger/ control animations than it usually is inside Bevy + Gltf files
+
+The workflow for animations is as follows:
+* create a gltf file with animations (using Blender & co) as you would normally do
+* inside Bevy, use the ```blenvy``` boilerplate (see sections above), no specific setup beyond that is required
+* to control the animation of an entity, you need to query for entities that have both ```AnimationPlayerLink``` and ```Animations``` components (added by ```blenvy```) AND entities with the ```AnimationPlayer``` component
+
+For example:
+
+```rust no_run
+// example of changing animation of entities based on proximity to the player, for "fox" entities (Tag component)
+pub fn animation_change_on_proximity_foxes(
+ players: Query<&GlobalTransform, With>,
+ animated_foxes: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations ), With>,
+
+ mut animation_players: Query<&mut AnimationPlayer>,
+
+){
+ for player_transforms in players.iter() {
+ for (fox_tranforms, link, animations) in animated_foxes.iter() {
+ let distance = player_transforms
+ .translation()
+ .distance(fox_tranforms.translation());
+ let mut anim_name = "Walk";
+ if distance < 8.5 {
+ anim_name = "Run";
+ }
+ else if distance >= 8.5 && distance < 10.0{
+ anim_name = "Walk";
+ }
+ else if distance >= 10.0 && distance < 15.0{
+ anim_name = "Survey";
+ }
+ // now play the animation based on the chosen animation name
+ let mut animation_player = animation_players.get_mut(link.0).unwrap();
+ animation_player.play_with_transition(
+ animations.named_animations.get(anim_name).expect("animation name should be in the list").clone(),
+ Duration::from_secs(3)
+ ).repeat();
+ }
+ }
+}
+```
+
+see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation for how to set it up correctly
+
+particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation/game/in_game.rs
+
+
+## Materials
+
+You have the option of using "material libraries" to share common textures/materials between blueprints, in order to avoid asset & memory bloat:
+
+Ie for example without this option, 56 different blueprints using the same material with a large texture would lead to the material/texture being embeded
+56 times !!
+
+
+you can configure this with the settings:
+```rust
+material_library: true // defaults to false, enable this to enable automatic injection of materials from material library files
+```
+
+> Important! you must take care of preloading your material librairy gltf files in advance, using for example ```bevy_asset_loader```since
+```blenvy``` currently does NOT take care of loading those at runtime
+
+
+see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/materials for how to set it up correctly
+
+Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
+
+
+## Examples
+
+https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic
+
+https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic_xpbd_physics
+
+https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation
+
+https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/materials
+
+https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/multiple_levels_multiple_blendfiles
+
+
+## Compatible Bevy versions
+
+The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
+
+Compatibility of `blenvy` versions:
+| `blenvy` | `bevy` |
+| :-- | :-- |
+| `0.1 - 0.2` | `0.14` |
+| branch `main` | `0.14` |
+| branch `bevy_main` | `main` |
+
+
+## License
+
+This crate, all its code, contents & assets is Dual-licensed under either of
+
+- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
+- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)
\ No newline at end of file
diff --git a/crates/blenvy/src/blueprints/aabb.rs b/crates/blenvy/src/blueprints/aabb.rs
new file mode 100644
index 0000000..84c2204
--- /dev/null
+++ b/crates/blenvy/src/blueprints/aabb.rs
@@ -0,0 +1,61 @@
+use bevy::{math::Vec3A, prelude::*, render::primitives::Aabb};
+
+use crate::{BlenvyConfig, Spawned};
+
+/// helper system that computes the compound aabbs of the scenes/blueprints
+pub fn compute_scene_aabbs(
+ root_entities: Query<(Entity, &Name), (With, Without)>,
+ children: Query<&Children>,
+ existing_aabbs: Query<&Aabb>,
+
+ mut blenvy_config: ResMut,
+ mut commands: Commands,
+) {
+ // compute compound aabb
+ for (root_entity, name) in root_entities.iter() {
+ // info!("generating aabb for {:?}", name);
+
+ // only recompute aabb if it has not already been done before
+ if blenvy_config.aabb_cache.contains_key(&name.to_string()) {
+ let aabb = blenvy_config
+ .aabb_cache
+ .get(&name.to_string())
+ .expect("we should have the aabb available");
+ commands.entity(root_entity).insert(*aabb);
+ } else {
+ let aabb = compute_descendant_aabb(root_entity, &children, &existing_aabbs);
+ commands.entity(root_entity).insert(aabb);
+ blenvy_config.aabb_cache.insert(name.to_string(), aabb);
+ }
+ }
+}
+
+pub fn compute_descendant_aabb(
+ root_entity: Entity,
+ children: &Query<&Children>,
+ existing_aabbs: &Query<&Aabb>,
+) -> Aabb {
+ if let Ok(children_list) = children.get(root_entity) {
+ let mut chilren_aabbs: Vec = vec![];
+ for child in children_list.iter() {
+ if let Ok(aabb) = existing_aabbs.get(*child) {
+ chilren_aabbs.push(*aabb);
+ } else {
+ let aabb = compute_descendant_aabb(*child, children, existing_aabbs);
+ chilren_aabbs.push(aabb);
+ }
+ }
+
+ let mut min = Vec3A::splat(f32::MAX);
+ let mut max = Vec3A::splat(f32::MIN);
+ for aabb in chilren_aabbs.iter() {
+ min = min.min(aabb.min());
+ max = max.max(aabb.max());
+ }
+ let aabb = Aabb::from_min_max(Vec3::from(min), Vec3::from(max));
+
+ return aabb;
+ }
+
+ Aabb::default()
+}
diff --git a/crates/blenvy/src/blueprints/animation.rs b/crates/blenvy/src/blueprints/animation.rs
new file mode 100644
index 0000000..6a61a65
--- /dev/null
+++ b/crates/blenvy/src/blueprints/animation.rs
@@ -0,0 +1,221 @@
+use bevy::prelude::*;
+use bevy::utils::HashMap;
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// storage for animations for a given entity's BLUEPRINT (ie for example a characters animations), essentially a clone of gltf's `named_animations`
+pub struct BlueprintAnimations {
+ pub named_animations: HashMap>,
+}
+
+#[derive(Component, Debug)]
+/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
+/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
+/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
+/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
+pub struct BlueprintAnimationPlayerLink(pub Entity);
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// storage for scene level animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations`
+pub struct SceneAnimations {
+ pub named_animations: HashMap>,
+}
+
+#[derive(Component, Debug)]
+/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
+/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
+/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
+/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
+pub struct SceneAnimationPlayerLink(pub Entity);
+
+/// Stores Animation information: name, frame informations etc
+#[derive(Reflect, Default, Debug)]
+pub struct AnimationInfo {
+ pub name: String,
+ pub frame_start: f32,
+ pub frame_end: f32,
+ pub frames_length: f32,
+ pub frame_start_override: f32,
+ pub frame_end_override: f32,
+}
+
+/// Stores information about animations, to make things a bit easier api wise:
+/// these components are automatically inserted by `gltf_auto_export` on entities that have animations
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct AnimationInfos {
+ pub animations: Vec,
+}
+
+#[derive(Reflect, Default, Debug)]
+pub struct AnimationMarker {
+ // pub frame: u32,
+ pub name: String,
+ pub handled_for_cycle: bool,
+}
+
+/// Stores information about animation markers: practical for adding things like triggering events at specific keyframes etc
+/// it is essentiall a hashmap of `AnimationName` => `HashMap`<`FrameNumber`, Vec of marker names>
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct AnimationMarkers(pub HashMap>>);
+
+/// Event that gets triggered once a specific marker inside an animation has been reached (frame based)
+/// Provides some usefull information about which entity , wich animation, wich frame & which marker got triggered
+#[derive(Event, Debug)]
+pub struct AnimationMarkerReached {
+ pub entity: Entity,
+ pub animation_name: String,
+ pub frame: u32,
+ pub marker_name: String,
+}
+
+/////////////////////
+
+/*
+/// triggers events when a given animation marker is reached for INSTANCE animations
+pub fn trigger_instance_animation_markers_events(
+ animation_infos: Query<(
+ Entity,
+ &AnimationMarkers,
+ &SceneAnimationPlayerLink,
+ &SceneAnimations,
+ &AnimationInfos,
+ )>,
+ animation_players: Query<(&AnimationPlayer)>,
+ animation_clips: Res>,
+ animation_graphs: Res>,
+ mut animation_marker_events: EventWriter,
+) {
+ for (entity, markers, link, animations, animation_infos) in animation_infos.iter() {
+ let animation_player = animation_players.get(link.0).unwrap();
+ let animation_clip = animation_clips.get(animation_player.animation_clip());
+ // animation_player.play(animation)
+
+ if animation_clip.is_some() {
+ // println!("Entity {:?} markers {:?}", entity, markers);
+ // println!("Player {:?} {}", animation_player.elapsed(), animation_player.completions());
+ // FIMXE: yikes ! very inneficient ! perhaps add boilerplate to the "start playing animation" code so we know what is playing
+ let animation_name = animations.named_animations.iter().find_map(|(key, value)| {
+ if value == animation_player.animation_clip() {
+ Some(key)
+ } else {
+ None
+ }
+ });
+ if animation_name.is_some() {
+ let animation_name = animation_name.unwrap();
+
+ let animation_length_seconds = animation_clip.unwrap().duration();
+ let animation_length_frames = animation_infos
+ .animations
+ .iter()
+ .find(|anim| &anim.name == animation_name)
+ .unwrap()
+ .frames_length;
+ // TODO: we also need to take playback speed into account
+ let time_in_animation = animation_player.elapsed()
+ - (animation_player.completions() as f32) * animation_length_seconds;
+ let frame_seconds =
+ (animation_length_frames / animation_length_seconds) * time_in_animation;
+ let frame = frame_seconds as u32;
+
+ let matching_animation_marker = &markers.0[animation_name];
+ if matching_animation_marker.contains_key(&frame) {
+ let matching_markers_per_frame = matching_animation_marker.get(&frame).unwrap();
+
+ // let timediff = animation_length_seconds - time_in_animation;
+ // println!("timediff {}", timediff);
+ // println!("FOUND A MARKER {:?} at frame {}", matching_markers_per_frame, frame);
+ // emit an event AnimationMarkerReached(entity, animation_name, frame, marker_name)
+ // FIXME: problem, this can fire multiple times in a row, depending on animation length , speed , etc
+ for marker in matching_markers_per_frame {
+ animation_marker_events.send(AnimationMarkerReached {
+ entity,
+ animation_name: animation_name.clone(),
+ frame,
+ marker_name: marker.clone(),
+ });
+ }
+ }
+ }
+ }
+ }
+}
+
+/// triggers events when a given animation marker is reached for BLUEPRINT animations
+pub fn trigger_blueprint_animation_markers_events(
+ animation_infos: Query<(Entity, &BlueprintAnimationPlayerLink, &BlueprintAnimations)>,
+ // FIXME: annoying hiearchy issue yet again: the Markers & AnimationInfos are stored INSIDE the blueprint, so we need to access them differently
+ all_animation_infos: Query<(Entity, &AnimationMarkers, &AnimationInfos, &Parent)>,
+ animation_players: Query<&AnimationPlayer>,
+ animation_clips: Res>,
+ mut animation_marker_events: EventWriter,
+) {
+ for (entity, link, animations) in animation_infos.iter() {
+ let animation_player = animation_players.get(link.0).unwrap();
+ let animation_clip = animation_clips.get(animation_player.animation_clip());
+
+ // FIXME: horrible code
+ for (_, markers, animation_infos, parent) in all_animation_infos.iter() {
+ if parent.get() == entity {
+ if animation_clip.is_some() {
+ // println!("Entity {:?} markers {:?}", entity, markers);
+ // println!("Player {:?} {}", animation_player.elapsed(), animation_player.completions());
+ // FIMXE: yikes ! very inneficient ! perhaps add boilerplate to the "start playing animation" code so we know what is playing
+ let animation_name =
+ animations.named_animations.iter().find_map(|(key, value)| {
+ if value == animation_player.animation_clip() {
+ Some(key)
+ } else {
+ None
+ }
+ });
+ if animation_name.is_some() {
+ let animation_name = animation_name.unwrap();
+ let animation_length_seconds = animation_clip.unwrap().duration();
+ let animation_length_frames = animation_infos
+ .animations
+ .iter()
+ .find(|anim| &anim.name == animation_name)
+ .unwrap()
+ .frames_length;
+ // TODO: we also need to take playback speed into account
+ let time_in_animation = animation_player.elapsed()
+ - (animation_player.completions() as f32) * animation_length_seconds;
+ let frame_seconds = (animation_length_frames / animation_length_seconds)
+ * time_in_animation;
+ // println!("frame seconds {}", frame_seconds);
+ let frame = frame_seconds.ceil() as u32; // FIXME , bad hack
+
+ let matching_animation_marker = &markers.0[animation_name];
+
+ if matching_animation_marker.contains_key(&frame) {
+ let matching_markers_per_frame =
+ matching_animation_marker.get(&frame).unwrap();
+ // println!("FOUND A MARKER {:?} at frame {}", matching_markers_per_frame, frame);
+ // emit an event AnimationMarkerReached(entity, animation_name, frame, marker_name)
+ // FIXME: complete hack-ish solution , otherwise this can fire multiple times in a row, depending on animation length , speed , etc
+ let diff = frame as f32 - frame_seconds;
+ println!("diff {}", diff);
+ if diff < 0.1 {
+ for marker in matching_markers_per_frame {
+ animation_marker_events.send(AnimationMarkerReached {
+ entity,
+ animation_name: animation_name.clone(),
+ frame,
+ marker_name: marker.clone(),
+ });
+ }
+ }
+ }
+ }
+ }
+
+ break;
+ }
+ }
+ }
+}
+*/
\ No newline at end of file
diff --git a/crates/blenvy/src/blueprints/assets.rs b/crates/blenvy/src/blueprints/assets.rs
new file mode 100644
index 0000000..02f3fe3
--- /dev/null
+++ b/crates/blenvy/src/blueprints/assets.rs
@@ -0,0 +1,63 @@
+use std::path::{Path, PathBuf};
+
+use bevy::{asset::LoadedUntypedAsset, gltf::Gltf, prelude::*, utils::HashMap};
+
+use crate::{BlenvyConfig, BlueprintAnimations};
+
+/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct MyAsset{
+ pub name: String,
+ pub path: String
+}
+
+/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct LocalAssets(pub Vec);
+
+/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct AllAssets(pub Vec);
+
+
+
+////////////////////////
+///
+/// flag component, usually added when a blueprint is loaded
+#[derive(Component)]
+pub(crate) struct BlueprintAssetsLoaded;
+/// flag component
+#[derive(Component)]
+pub(crate) struct BlueprintAssetsNotLoaded;
+
+/// helper component, for tracking loaded assets's loading state, id , handle etc
+#[derive(Debug)]
+pub(crate) struct AssetLoadTracker {
+ #[allow(dead_code)]
+ pub name: String,
+ pub id: AssetId,
+ pub loaded: bool,
+ #[allow(dead_code)]
+ pub handle: Handle,
+}
+
+
+/// helper component, for tracking loaded assets
+#[derive(Component, Debug)]
+pub(crate) struct AssetsToLoad {
+ pub all_loaded: bool,
+ pub asset_infos: Vec,
+ pub progress: f32,
+}
+impl Default for AssetsToLoad {
+ fn default() -> Self {
+ Self {
+ all_loaded: Default::default(),
+ asset_infos: Default::default(),
+ progress: Default::default(),
+ }
+ }
+}
diff --git a/crates/blenvy/src/blueprints/copy_components.rs b/crates/blenvy/src/blueprints/copy_components.rs
new file mode 100644
index 0000000..b2a12b5
--- /dev/null
+++ b/crates/blenvy/src/blueprints/copy_components.rs
@@ -0,0 +1,107 @@
+use bevy::ecs::world::Command;
+use bevy::prelude::*;
+use std::any::TypeId;
+
+// originally based https://github.com/bevyengine/bevy/issues/1515,
+// more specifically https://gist.github.com/nwtnni/85d6b87ae75337a522166c500c9a8418
+// to work with Bevy 0.11
+// to copy components between entities but NOT overwriting any existing components
+// plus some bells & whistles
+pub struct CopyComponents {
+ pub source: Entity,
+ pub destination: Entity,
+ pub exclude: Vec,
+ pub stringent: bool,
+}
+
+impl CopyComponents {
+ // Copy all components from an entity to another.
+ // Using an entity with no components as the destination creates a copy of the source entity.
+ // Panics if:
+ // - the components are not registered in the type registry,
+ // - the world does not have a type registry
+ // - the source or destination entity do not exist
+ fn transfer_components(self, world: &mut World) {
+ let components = {
+ let registry = world
+ .get_resource::()
+ .expect("the world should have a type registry")
+ .read();
+
+ world
+ .get_entity(self.source)
+ .expect("source entity should exist")
+ .archetype()
+ .components()
+ .filter_map(|component_id| {
+ let component_info = world
+ .components()
+ .get_info(component_id)
+ .expect("component info should be available");
+
+ let type_id = component_info.type_id().unwrap();
+ if self.exclude.contains(&type_id) {
+ debug!("excluding component: {:?}", component_info.name());
+ None
+ } else {
+ debug!(
+ "cloning: component: {:?} {:?}",
+ component_info.name(),
+ type_id
+ );
+
+ if let Some(type_registration) = registry.get(type_id) {
+ Some(type_registration)
+ } else if self.stringent {
+ return Some(registry.get(type_id).unwrap_or_else(|| {
+ panic!(
+ "cannot clone entity: component: {:?} is not registered",
+ component_info.name()
+ )
+ }));
+ } else {
+ warn!(
+ "cannot clone component: component: {:?} is not registered",
+ component_info.name()
+ );
+ None
+ }
+ }
+ })
+ .map(|type_id| {
+ return (
+ type_id.data::().unwrap().clone(),
+ type_id.type_info().type_id(), // we need the original type_id down the line
+ );
+ })
+ .collect::>()
+ };
+
+ for (component, type_id) in components {
+ let type_registry: &AppTypeRegistry = world.resource();
+ let type_registry = type_registry.clone();
+ let type_registry = type_registry.read();
+ let source = component
+ .reflect(world.get_entity(self.source).unwrap())
+ .unwrap()
+ .clone_value();
+
+ let mut destination = world
+ .get_entity_mut(self.destination)
+ .expect("destination entity should exist");
+
+ // println!("contains typeid {:?} {}", type_id, destination.contains_type_id(type_id));
+ // we only want to copy components that are NOT already in the destination (ie no overwriting existing components)
+ if !destination.contains_type_id(type_id) {
+ component.insert(&mut destination, &*source, &type_registry);
+ }
+ }
+ }
+}
+
+// This allows the command to be used in systems
+impl Command for CopyComponents {
+ fn apply(self, world: &mut World) {
+ self.transfer_components(world);
+ }
+}
diff --git a/crates/blenvy/src/blueprints/materials.rs b/crates/blenvy/src/blueprints/materials.rs
new file mode 100644
index 0000000..a2a704a
--- /dev/null
+++ b/crates/blenvy/src/blueprints/materials.rs
@@ -0,0 +1,189 @@
+use std::path::Path;
+
+use bevy::{
+ asset::{AssetServer, Assets, Handle},
+ ecs::{
+ component::Component,
+ entity::Entity,
+ query::{Added, With},
+ reflect::ReflectComponent,
+ system::{Commands, Query, Res, ResMut},
+ },
+ gltf::Gltf,
+ hierarchy::{Children, Parent},
+ log::debug,
+ pbr::StandardMaterial,
+ reflect::Reflect,
+ render::mesh::Mesh,
+};
+
+use crate::{AssetLoadTracker, AssetsToLoad, BlenvyConfig, BlueprintInstanceReady};
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// struct containing the name & path of the material to apply
+pub struct MaterialInfo {
+ pub name: String,
+ pub path: String,
+}
+
+/// flag component
+#[derive(Component)]
+pub(crate) struct BlueprintMaterialAssetsLoaded;
+/// flag component
+#[derive(Component)]
+pub(crate) struct BlueprintMaterialAssetsNotLoaded;
+
+/// system that injects / replaces materials from material library
+pub(crate) fn materials_inject(
+ blenvy_config: ResMut,
+ material_infos: Query<(Entity, &MaterialInfo), Added>,
+ asset_server: Res,
+ mut commands: Commands,
+) {
+
+
+ for (entity, material_info) in material_infos.iter() {
+ println!("Entity with material info {:?} {:?}", entity, material_info);
+ let material_full_path = format!("{}#{}", material_info.path, material_info.name);
+ if blenvy_config
+ .materials_cache
+ .contains_key(&material_full_path)
+ {
+ debug!("material is cached, retrieving");
+ blenvy_config
+ .materials_cache
+ .get(&material_full_path)
+ .expect("we should have the material available");
+ commands
+ .entity(entity)
+ .insert(BlueprintMaterialAssetsLoaded);
+ } else {
+ let material_file_handle = asset_server.load_untyped(&material_info.path.clone()); // : Handle
+ let material_file_id = material_file_handle.id();
+
+ let asset_infos: Vec = vec![AssetLoadTracker {
+ name: material_info.name.clone(),
+ id: material_file_id,
+ loaded: false,
+ handle: material_file_handle.clone(),
+ }];
+
+ commands
+ .entity(entity)
+ .insert(AssetsToLoad {
+ all_loaded: false,
+ asset_infos,
+ ..Default::default()
+ })
+ .insert(BlueprintMaterialAssetsNotLoaded);
+
+ }
+ }
+}
+
+// TODO, merge with blueprints_check_assets_loading, make generic ?
+pub(crate) fn check_for_material_loaded(
+ mut blueprint_assets_to_load: Query<
+ (Entity, &mut AssetsToLoad),
+ With,
+ >,
+ asset_server: Res,
+ mut commands: Commands,
+) {
+ for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
+ let mut all_loaded = true;
+ let mut loaded_amount = 0;
+ let total = assets_to_load.asset_infos.len();
+ for tracker in assets_to_load.asset_infos.iter_mut() {
+ let asset_id = tracker.id;
+ let loaded = asset_server.is_loaded_with_dependencies(asset_id);
+ tracker.loaded = loaded;
+ if loaded {
+ loaded_amount += 1;
+ } else {
+ all_loaded = false;
+ }
+ }
+ let progress: f32 = loaded_amount as f32 / total as f32;
+ assets_to_load.progress = progress;
+
+ if all_loaded {
+ assets_to_load.all_loaded = true;
+ commands
+ .entity(entity)
+ .insert(BlueprintMaterialAssetsLoaded)
+ .remove::();
+ }
+ }
+}
+
+/// system that injects / replaces materials from material library
+pub(crate) fn materials_inject2(
+ mut blenvy_config: ResMut,
+ material_infos: Query<
+ (&MaterialInfo, &Children),
+ (
+ Added,
+ With,
+ ),
+ >,
+ with_materials_and_meshes: Query<
+ (),
+ (
+ With,
+ With>,
+ With>,
+ ),
+ >,
+ assets_gltf: Res>,
+ asset_server: Res,
+
+ mut commands: Commands,
+) {
+ for (material_info, children) in material_infos.iter() {
+ let material_full_path = format!("{}#{}", material_info.path, material_info.name);
+ let mut material_found: Option<&Handle> = None;
+
+ if blenvy_config
+ .materials_cache
+ .contains_key(&material_full_path)
+ {
+ debug!("material is cached, retrieving");
+ let material = blenvy_config
+ .materials_cache
+ .get(&material_full_path)
+ .expect("we should have the material available");
+ material_found = Some(material);
+ } else {
+ let model_handle: Handle = asset_server.load(material_info.path.clone()); // FIXME: kinda weird now
+ let mat_gltf = assets_gltf
+ .get(model_handle.id())
+ .expect("material should have been preloaded");
+ if mat_gltf.named_materials.contains_key(&material_info.name as &str) {
+ let material = mat_gltf
+ .named_materials
+ .get(&material_info.name as &str)
+ .expect("this material should have been loaded");
+ blenvy_config
+ .materials_cache
+ .insert(material_full_path, material.clone());
+ material_found = Some(material);
+ }
+ }
+
+ if let Some(material) = material_found {
+ for child in children.iter() {
+ if with_materials_and_meshes.contains(*child) {
+ debug!(
+ "injecting material {}, path: {:?}",
+ material_info.name,
+ material_info.path.clone()
+ );
+
+ commands.entity(*child).insert(material.clone());
+ }
+ }
+ }
+ }
+}
diff --git a/crates/blenvy/src/blueprints/mod.rs b/crates/blenvy/src/blueprints/mod.rs
new file mode 100644
index 0000000..3a52047
--- /dev/null
+++ b/crates/blenvy/src/blueprints/mod.rs
@@ -0,0 +1,172 @@
+pub mod spawn_from_blueprints;
+pub use spawn_from_blueprints::*;
+
+pub mod spawn_post_process;
+pub(crate) use spawn_post_process::*;
+
+pub mod animation;
+pub use animation::*;
+
+pub mod aabb;
+pub use aabb::*;
+
+pub mod assets;
+pub use assets::*;
+
+pub mod materials;
+pub use materials::*;
+
+pub mod copy_components;
+pub use copy_components::*;
+
+use core::fmt;
+use std::path::PathBuf;
+
+use bevy::{prelude::*, render::primitives::Aabb, utils::HashMap};
+
+use crate::{BlenvyConfig, GltfComponentsSet};
+
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+/// set for the two stages of blueprint based spawning :
+pub enum GltfBlueprintsSet {
+ Spawn,
+ AfterSpawn,
+}
+
+#[derive(Bundle)]
+pub struct BluePrintBundle {
+ pub blueprint: BlueprintName,
+ pub blueprint_path: BlueprintPath,
+ pub spawn_here: SpawnHere,
+}
+impl Default for BluePrintBundle {
+ fn default() -> Self {
+ BluePrintBundle {
+ blueprint: BlueprintName("default".into()),
+ blueprint_path: BlueprintPath("".into()),
+ spawn_here: SpawnHere,
+ }
+ }
+}
+
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
+pub enum GltfFormat {
+ #[default]
+ GLB,
+ GLTF,
+}
+
+impl fmt::Display for GltfFormat {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ GltfFormat::GLB => {
+ write!(f, "glb",)
+ }
+ GltfFormat::GLTF => {
+ write!(f, "gltf")
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+/// Plugin for gltf blueprints
+pub struct BlueprintsPlugin {
+ /// Automatically generate aabbs for the blueprints root objects
+ pub aabbs: bool,
+ ///
+ pub material_library: bool,
+}
+
+impl Default for BlueprintsPlugin {
+ fn default() -> Self {
+ Self {
+ aabbs: false,
+ material_library: false
+ }
+ }
+}
+
+fn aabbs_enabled(blenvy_config: Res) -> bool {
+ blenvy_config.aabbs
+}
+
+
+impl Plugin for BlueprintsPlugin {
+ fn build(&self, app: &mut App) {
+ app
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::()
+ .register_type::>()
+ .register_type::()
+ .register_type::>>()
+ .register_type::>>>()
+ .add_event::()
+ .register_type::()
+ .register_type::>()
+ .register_type::>()
+ .register_type::()
+ .register_type::()
+
+ .add_event::()
+
+
+
+ .register_type::>>()
+ .configure_sets(
+ Update,
+ (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)
+ .chain()
+ .after(GltfComponentsSet::Injection),
+ )
+ .add_systems(
+ Update,
+ (
+ blueprints_prepare_spawn,
+ blueprints_check_assets_loading,
+ blueprints_spawn,
+
+ /*(
+ prepare_blueprints,
+ blueprints_check_assets_loading,
+ blueprints_spawn,
+ apply_deferred,
+ )
+ .chain(),*/
+ (compute_scene_aabbs, apply_deferred)
+ .chain()
+ .run_if(aabbs_enabled),
+ apply_deferred,
+ (
+ materials_inject,
+ check_for_material_loaded,
+ materials_inject2,
+ )
+ .chain()
+ )
+ .chain()
+ .in_set(GltfBlueprintsSet::Spawn),
+ )
+ .add_systems(
+ Update,
+ (spawned_blueprint_post_process, apply_deferred)
+ .chain()
+ .in_set(GltfBlueprintsSet::AfterSpawn),
+ )
+ /* .add_systems(
+ Update,
+ (
+ trigger_instance_animation_markers_events,
+ trigger_blueprint_animation_markers_events,
+ ),
+ )*/
+ ;
+ }
+}
diff --git a/crates/blenvy/src/blueprints/old.rs b/crates/blenvy/src/blueprints/old.rs
new file mode 100644
index 0000000..20724e5
--- /dev/null
+++ b/crates/blenvy/src/blueprints/old.rs
@@ -0,0 +1,248 @@
+
+
+/// helper component, for tracking loaded assets's loading state, id , handle etc
+#[derive(Default, Debug)]
+pub(crate) struct AssetLoadTracker {
+ #[allow(dead_code)]
+ pub name: String,
+ pub id: AssetId,
+ pub loaded: bool,
+ #[allow(dead_code)]
+ pub handle: Handle,
+}
+
+/// helper component, for tracking loaded assets
+#[derive(Component, Debug)]
+pub(crate) struct AssetsToLoad {
+ pub all_loaded: bool,
+ pub asset_infos: Vec>,
+ pub progress: f32,
+}
+impl Default for AssetsToLoad {
+ fn default() -> Self {
+ Self {
+ all_loaded: Default::default(),
+ asset_infos: Default::default(),
+ progress: Default::default(),
+ }
+ }
+}
+
+/// flag component, usually added when a blueprint is loaded
+#[derive(Component)]
+pub(crate) struct BlueprintAssetsLoaded;
+/// flag component
+#[derive(Component)]
+pub(crate) struct BlueprintAssetsNotLoaded;
+
+
+
+/// spawning prepare function,
+/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
+pub(crate) fn prepare_blueprints(
+ spawn_placeholders: Query<
+ (
+ Entity,
+ &BlueprintName,
+ Option<&Parent>,
+ Option<&Library>,
+ Option<&Name>,
+ Option<&BlueprintsList>,
+ ),
+ (Added, Added, Without),
+ >,
+
+ mut commands: Commands,
+ asset_server: Res,
+ blenvy_config: Res,
+) {
+ for (entity, blupeprint_name, original_parent, library_override, name, blueprints_list) in
+ spawn_placeholders.iter()
+ {
+ debug!(
+ "requesting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
+ blupeprint_name.0, name, entity, original_parent
+ );
+
+ // println!("main model path {:?}", model_path);
+ if blueprints_list.is_some() {
+ let blueprints_list = blueprints_list.unwrap();
+ // println!("blueprints list {:?}", blueprints_list.0.keys());
+ let mut asset_infos: Vec> = vec![];
+ let library_path =
+ library_override.map_or_else(|| &blenvy_config.library_folder, |l| &l.0);
+ for (blueprint_name, _) in blueprints_list.0.iter() {
+ let model_file_name = format!("{}.{}", &blueprint_name, &blenvy_config.format);
+ let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
+
+ let model_handle: Handle = asset_server.load(model_path.clone());
+ let model_id = model_handle.id();
+ let loaded = asset_server.is_loaded_with_dependencies(model_id);
+ if !loaded {
+ asset_infos.push(AssetLoadTracker {
+ name: model_path.to_string_lossy().into(),
+ id: model_id,
+ loaded: false,
+ handle: model_handle.clone(),
+ });
+ }
+ }
+ // if not all assets are already loaded, inject a component to signal that we need them to be loaded
+ if !asset_infos.is_empty() {
+ commands
+ .entity(entity)
+ .insert(AssetsToLoad {
+ all_loaded: false,
+ asset_infos,
+ ..Default::default()
+ })
+ .insert(BlueprintAssetsNotLoaded);
+ } else {
+ commands.entity(entity).insert(BlueprintAssetsLoaded);
+ }
+ } else {
+ // in case there are no blueprintsList, we revert back to the old behaviour
+ commands.entity(entity).insert(BlueprintAssetsLoaded);
+ }
+ }
+}
+
+pub(crate) fn blueprints_check_assets_loading(
+ mut blueprint_assets_to_load: Query<
+ (Entity, &mut AssetsToLoad),
+ With,
+ >,
+ asset_server: Res,
+ mut commands: Commands,
+) {
+ for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
+ let mut all_loaded = true;
+ let mut loaded_amount = 0;
+ let total = assets_to_load.asset_infos.len();
+ for tracker in assets_to_load.asset_infos.iter_mut() {
+ let asset_id = tracker.id;
+ let loaded = asset_server.is_loaded_with_dependencies(asset_id);
+ tracker.loaded = loaded;
+ if loaded {
+ loaded_amount += 1;
+ } else {
+ all_loaded = false;
+ }
+ }
+ let progress: f32 = loaded_amount as f32 / total as f32;
+ // println!("progress: {}",progress);
+ assets_to_load.progress = progress;
+
+ if all_loaded {
+ assets_to_load.all_loaded = true;
+ commands
+ .entity(entity)
+ .insert(BlueprintAssetsLoaded)
+ .remove::();
+ }
+ }
+}
+
+pub(crate) fn blueprints_spawn(
+ spawn_placeholders: Query<
+ (
+ Entity,
+ &BlueprintName,
+ Option<&Transform>,
+ Option<&Parent>,
+ Option<&Library>,
+ Option<&AddToGameWorld>,
+ Option<&Name>,
+ ),
+ (
+ With,
+ Added,
+ Without,
+ ),
+ >,
+
+ mut commands: Commands,
+ mut game_world: Query>,
+
+ assets_gltf: Res>,
+ asset_server: Res,
+ blenvy_config: Res,
+
+ children: Query<&Children>,
+) {
+ for (
+ entity,
+ blupeprint_name,
+ transform,
+ original_parent,
+ library_override,
+ add_to_world,
+ name,
+ ) in spawn_placeholders.iter()
+ {
+ debug!(
+ "attempting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
+ blupeprint_name.0, name, entity, original_parent
+ );
+
+ let what = &blupeprint_name.0;
+ let model_file_name = format!("{}.{}", &what, &blenvy_config.format);
+
+ // library path is either defined at the plugin level or overriden by optional Library components
+ let library_path =
+ library_override.map_or_else(|| &blenvy_config.library_folder, |l| &l.0);
+ let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
+
+ // info!("attempting to spawn {:?}", model_path);
+ let model_handle: Handle = asset_server.load(model_path.clone()); // FIXME: kinda weird now
+
+ let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
+ panic!(
+ "gltf file {:?} should have been loaded",
+ model_path.to_str()
+ )
+ });
+
+ // WARNING we work under the assumtion that there is ONLY ONE named scene, and that the first one is the right one
+ let main_scene_name = gltf
+ .named_scenes
+ .keys()
+ .next()
+ .expect("there should be at least one named scene in the gltf file to spawn");
+
+ let scene = &gltf.named_scenes[main_scene_name];
+
+ // transforms are optional, but still deal with them correctly
+ let mut transforms: Transform = Transform::default();
+ if transform.is_some() {
+ transforms = *transform.unwrap();
+ }
+
+ let mut original_children: Vec = vec![];
+ if let Ok(c) = children.get(entity) {
+ for child in c.iter() {
+ original_children.push(*child);
+ }
+ }
+ commands.entity(entity).insert((
+ SceneBundle {
+ scene: scene.clone(),
+ transform: transforms,
+ ..Default::default()
+ },
+ Spawned,
+ OriginalChildren(original_children),
+ BlueprintAnimations {
+ // these are animations specific to the inside of the blueprint
+ named_animations: gltf.named_animations.clone(),
+ },
+ ));
+
+ if add_to_world.is_some() {
+ let world = game_world
+ .get_single_mut()
+ .expect("there should be a game world present");
+ commands.entity(world).add_child(entity);
+ }
+ }
+}
diff --git a/crates/blenvy/src/blueprints/spawn_from_blueprints.rs b/crates/blenvy/src/blueprints/spawn_from_blueprints.rs
new file mode 100644
index 0000000..5d84865
--- /dev/null
+++ b/crates/blenvy/src/blueprints/spawn_from_blueprints.rs
@@ -0,0 +1,365 @@
+use std::path::{Path, PathBuf};
+
+use bevy::{gltf::Gltf, prelude::*, utils::hashbrown::HashMap};
+
+use crate::{AllAssets, AssetsToLoad, AssetLoadTracker, BlenvyConfig, BlueprintAnimations, BlueprintAssetsLoaded, BlueprintAssetsNotLoaded};
+
+/// this is a flag component for our levels/game world
+#[derive(Component)]
+pub struct GameWorldTag;
+
+/// Main component for the blueprints
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct BlueprintName(pub String);
+
+/// path component for the blueprints
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct BlueprintPath(pub String);
+
+/// flag component needed to signify the intent to spawn a Blueprint
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct SpawnHere;
+
+#[derive(Component)]
+/// flag component for dynamically spawned scenes
+pub struct Spawned;
+
+
+#[derive(Component, Debug)]
+/// flag component added when a Blueprint instance ist Ready : ie :
+/// - its assets have loaded
+/// - it has finished spawning
+pub struct BlueprintInstanceReady;
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// flag component marking any spwaned child of blueprints ..unless the original entity was marked with the `NoInBlueprint` marker component
+pub struct InBlueprint;
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// flag component preventing any spawned child of blueprints to be marked with the `InBlueprint` component
+pub struct NoInBlueprint;
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+// this allows overriding the default library path for a given entity/blueprint
+pub struct Library(pub PathBuf);
+
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+/// flag component to force adding newly spawned entity as child of game world
+pub struct AddToGameWorld;
+
+#[derive(Component)]
+/// helper component, just to transfer child data
+pub(crate) struct OriginalChildren(pub Vec);
+
+
+#[derive(Event, Debug)]
+pub enum BlueprintEvent {
+
+ /// event fired when a blueprint has finished loading its assets & before it attempts spawning
+ AssetsLoaded {
+ blueprint_name: String,
+ blueprint_path: String,
+ // TODO: add assets list ?
+ },
+ /// event fired when a blueprint is COMPLETELY done spawning ie
+ /// - all its assets have been loaded
+ /// - the spawning attempt has been sucessfull
+ Spawned {
+ blueprint_name: String,
+ blueprint_path: String,
+ },
+
+ ///
+ Ready {
+ blueprint_path: String,
+ }
+
+}
+
+pub(crate) fn blueprints_prepare_spawn(
+ spawn_placeholders: Query<
+ (
+ Entity,
+ &BlueprintPath,
+ ),
+ (Added, Without, Without)>,
+
+ // before 0.14 we have to use a seperate query, after migrating we can query at the root level
+ entities_with_assets: Query<
+ (
+ Entity,
+ /*&BlueprintName,
+ &BlueprintPath,
+ Option<&Parent>,*/
+ Option<&Name>,
+ Option<&AllAssets>,
+ ),
+ (Added), // Added
+ >,
+
+
+ bla_bla : Query<
+ (
+ Entity,
+ &BlueprintName,
+ &BlueprintPath,
+ Option<&Parent>,
+ Option<&AllAssets>,
+ ),(Added)
+ >,
+mut commands: Commands,
+asset_server: Res,
+
+
+) {
+ for (entity, blueprint_path) in spawn_placeholders.iter() {
+ //println!("added blueprint_path {:?}", blueprint_path);
+ /*commands.entity(entity).insert(
+ SceneBundle {
+ scene: asset_server.load(format!("{}#Scene0", &blueprint_path.0)), // "levels/World.glb#Scene0"),
+ ..default()
+ },
+ );*/
+ // let model_handle: Handle = asset_server.load(model_path.clone());
+ }
+
+ for (entity, blueprint_name, blueprint_path, parent, all_assets) in bla_bla.iter() {
+ println!("added blueprint to spawn {:?} {:?}", blueprint_name, blueprint_path);
+ // println!("all assets {:?}", all_assets);
+ let untyped_handle = asset_server.load_untyped(&blueprint_path.0);
+ let asset_id = untyped_handle.id();
+ let loaded = asset_server.is_loaded_with_dependencies(asset_id);
+
+ let mut asset_infos: Vec = vec![];
+ if !loaded {
+ asset_infos.push(AssetLoadTracker {
+ name: blueprint_name.0.clone(),
+ id: asset_id,
+ loaded: false,
+ handle: untyped_handle.clone(),
+ });
+ }
+
+ // now insert load tracker
+ if !asset_infos.is_empty() {
+ commands
+ .entity(entity)
+ .insert(AssetsToLoad {
+ all_loaded: false,
+ asset_infos,
+ ..Default::default()
+ })
+ .insert(BlueprintAssetsNotLoaded);
+ } else {
+ commands.entity(entity).insert(BlueprintAssetsLoaded);
+ }
+ }
+
+ for (child_entity, child_entity_name, all_assets) in entities_with_assets.iter(){
+ println!("added assets {:?} to {:?}", all_assets, child_entity_name);
+ if all_assets.is_some() {
+ let mut asset_infos: Vec = vec![];
+
+ for asset in all_assets.unwrap().0.iter() {
+ let untyped_handle = asset_server.load_untyped(&asset.path);
+ //println!("untyped handle {:?}", untyped_handle);
+ //asset_server.load(asset.path);
+
+ let asset_id = untyped_handle.id();
+ //println!("ID {:?}", asset_id);
+ let loaded = asset_server.is_loaded_with_dependencies(asset_id);
+ //println!("Loaded ? {:?}", loaded);
+ if !loaded {
+ asset_infos.push(AssetLoadTracker {
+ name: asset.name.clone(),
+ id: asset_id,
+ loaded: false,
+ handle: untyped_handle.clone(),
+ });
+ }
+ }
+
+ // now insert load tracker
+ if !asset_infos.is_empty() {
+ commands
+ .entity(child_entity)
+ .insert(AssetsToLoad {
+ all_loaded: false,
+ asset_infos,
+ ..Default::default()
+ })
+ .insert(BlueprintAssetsNotLoaded);
+ } else {
+ commands.entity(child_entity).insert(BlueprintAssetsLoaded);
+ }
+ }
+ }
+}
+
+pub(crate) fn blueprints_check_assets_loading(
+ mut blueprint_assets_to_load: Query<
+ (Entity, Option<&Name>, &BlueprintPath, &mut AssetsToLoad),
+ With,
+ >,
+ asset_server: Res,
+ mut commands: Commands,
+ mut blueprint_events: EventWriter,
+
+) {
+ for (entity, entity_name, blueprint_path, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
+ let mut all_loaded = true;
+ let mut loaded_amount = 0;
+ let total = assets_to_load.asset_infos.len();
+ for tracker in assets_to_load.asset_infos.iter_mut() {
+ let asset_id = tracker.id;
+ let loaded = asset_server.is_loaded_with_dependencies(asset_id);
+ println!("loading {}: // load state: {:?}", tracker.name, asset_server.load_state(asset_id));
+
+ // FIXME: hack for now
+ let mut failed = false;// asset_server.load_state(asset_id) == bevy::asset::LoadState::Failed(_error);
+ match asset_server.load_state(asset_id) {
+ bevy::asset::LoadState::Failed(_) => {
+ failed = true
+ },
+ _ => {}
+ }
+ tracker.loaded = loaded || failed;
+ if loaded || failed {
+ loaded_amount += 1;
+ } else {
+ all_loaded = false;
+ }
+ }
+ let progress: f32 = loaded_amount as f32 / total as f32;
+ println!("progress: {}",progress);
+ assets_to_load.progress = progress;
+
+ if all_loaded {
+ assets_to_load.all_loaded = true;
+ println!("done with loading {:?}, inserting components", entity_name);
+ blueprint_events.send(BlueprintEvent::AssetsLoaded {blueprint_name:"".into(), blueprint_path: blueprint_path.0.clone() });
+
+ commands
+ .entity(entity)
+ .insert(BlueprintAssetsLoaded)
+ .remove::()
+ .remove::()
+ ;
+ }
+ }
+}
+
+
+
+pub(crate) fn blueprints_spawn(
+ spawn_placeholders: Query<
+ (
+ Entity,
+ &BlueprintName,
+ &BlueprintPath,
+ Option<&Transform>,
+ Option<&Parent>,
+ Option<&AddToGameWorld>,
+ Option<&Name>,
+ ),
+ (
+ With,
+ Added,
+ Without,
+ ),
+ >,
+
+ mut commands: Commands,
+ mut game_world: Query>,
+
+ assets_gltf: Res>,
+ asset_server: Res,
+ children: Query<&Children>,
+) {
+ for (
+ entity,
+ blupeprint_name,
+ blueprint_path,
+ transform,
+ original_parent,
+ add_to_world,
+ name,
+ ) in spawn_placeholders.iter()
+ {
+ info!(
+ "attempting to spawn blueprint {:?} for entity {:?}, id: {:?}, parent:{:?}",
+ blupeprint_name.0, name, entity, original_parent
+ );
+
+ // info!("attempting to spawn {:?}", model_path);
+ let model_handle: Handle = asset_server.load(blueprint_path.0.clone()); // FIXME: kinda weird now
+
+ let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
+ panic!(
+ "gltf file {:?} should have been loaded",
+ &blueprint_path.0
+ )
+ });
+
+ // WARNING we work under the assumtion that there is ONLY ONE named scene, and that the first one is the right one
+ let main_scene_name = gltf
+ .named_scenes
+ .keys()
+ .next()
+ .expect("there should be at least one named scene in the gltf file to spawn");
+
+ let scene = &gltf.named_scenes[main_scene_name];
+
+ // transforms are optional, but still deal with them correctly
+ let mut transforms: Transform = Transform::default();
+ if transform.is_some() {
+ transforms = *transform.unwrap();
+ }
+
+ let mut original_children: Vec = vec![];
+ if let Ok(c) = children.get(entity) {
+ for child in c.iter() {
+ original_children.push(*child);
+ }
+ }
+
+ let mut named_animations:HashMap> = HashMap::new() ;
+ for (key, value) in gltf.named_animations.iter() {
+ named_animations.insert(key.to_string(), value.clone());
+ }
+
+ commands.entity(entity).insert((
+ SceneBundle {
+ scene: scene.clone(),
+ transform: transforms,
+ ..Default::default()
+ },
+ Spawned,
+ BlueprintInstanceReady, // FIXME: not sure if this is should be added here or in the post process
+ OriginalChildren(original_children),
+ BlueprintAnimations {
+ // these are animations specific to the inside of the blueprint
+ named_animations: named_animations//gltf.named_animations.clone(),
+ },
+ ));
+
+ if add_to_world.is_some() {
+ let world = game_world
+ .get_single_mut()
+ .expect("there should be a game world present");
+ commands.entity(world).add_child(entity);
+ }
+ }
+}
+
+
+
+
+
diff --git a/crates/blenvy/src/blueprints/spawn_post_process.rs b/crates/blenvy/src/blueprints/spawn_post_process.rs
new file mode 100644
index 0000000..ff25023
--- /dev/null
+++ b/crates/blenvy/src/blueprints/spawn_post_process.rs
@@ -0,0 +1,110 @@
+use std::any::TypeId;
+
+use bevy::gltf::Gltf;
+use bevy::prelude::*;
+use bevy::scene::SceneInstance;
+// use bevy::utils::hashbrown::HashSet;
+
+use crate::{BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintPath};
+use crate::{SpawnHere, Spawned};
+use crate::{
+ AssetsToLoad, BlueprintAssetsLoaded, BlueprintEvent, CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren
+};
+
+
+
+/// this system is in charge of doing any necessary post processing after a blueprint scene has been spawned
+/// - it removes one level of useless nesting
+/// - it copies the blueprint's root components to the entity it was spawned on (original entity)
+/// - it copies the children of the blueprint scene into the original entity
+/// - it add `AnimationLink` components so that animations can be controlled from the original entity
+/// - it cleans up/ removes a few , by then uneeded components
+pub(crate) fn spawned_blueprint_post_process(
+ unprocessed_entities: Query<
+ (
+ Entity,
+ &Children,
+ &OriginalChildren,
+ &BlueprintAnimations,
+ Option<&NoInBlueprint>,
+ Option<&Name>,
+ &BlueprintPath
+ ),
+ (With, With, With),
+ >,
+ added_animation_players: Query<(Entity, &Parent), Added>,
+ all_children: Query<&Children>,
+
+ mut commands: Commands,
+ mut blueprint_events: EventWriter,
+
+) {
+ for (original, children, original_children, animations, no_inblueprint, name, blueprint_path) in
+ unprocessed_entities.iter()
+ {
+ info!("post processing blueprint for entity {:?}", name);
+
+ if children.len() == 0 {
+ warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
+ continue;
+ }
+ // the root node is the first & normally only child inside a scene, it is the one that has all relevant components
+ let mut root_entity = Entity::PLACEHOLDER; //FIXME: and what about childless ones ?? => should not be possible normally
+ // let diff = HashSet::from_iter(original_children.0).difference(HashSet::from_iter(children));
+ // we find the first child that was not in the entity before (aka added during the scene spawning)
+ for c in children.iter() {
+ if !original_children.0.contains(c) {
+ root_entity = *c;
+ break;
+ }
+ }
+
+ // we flag all children of the blueprint instance with 'InBlueprint'
+ // can be usefull to filter out anything that came from blueprints vs normal children
+ if no_inblueprint.is_none() {
+ for child in all_children.iter_descendants(root_entity) {
+ commands.entity(child).insert(InBlueprint);
+ }
+ }
+
+ // copy components into from blueprint instance's root_entity to original entity
+ commands.add(CopyComponents {
+ source: root_entity,
+ destination: original,
+ exclude: vec![TypeId::of::(), TypeId::of::()],
+ stringent: false,
+ });
+
+ // we move all of children of the blueprint instance one level to the original entity
+ if let Ok(root_entity_children) = all_children.get(root_entity) {
+ for child in root_entity_children.iter() {
+ // info!("copying child {:?} upward from {:?} to {:?}", names.get(*child), root_entity, original);
+ commands.entity(original).add_child(*child);
+ }
+ }
+
+ if animations.named_animations.keys().len() > 0 {
+ for (added, parent) in added_animation_players.iter() {
+ if parent.get() == root_entity {
+ // FIXME: stopgap solution: since we cannot use an AnimationPlayer at the root entity level
+ // and we cannot update animation clips so that the EntityPaths point to one level deeper,
+ // BUT we still want to have some marker/control at the root entity level, we add this
+ commands
+ .entity(original)
+ .insert(BlueprintAnimationPlayerLink(added));
+ }
+ }
+ }
+
+ commands.entity(original).remove::();
+ commands.entity(original).remove::();
+ // commands.entity(original).remove::>(); // FIXME: if we delete the handle to the scene, things get despawned ! not what we want
+ //commands.entity(original).remove::(); // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ?
+ //commands.entity(original).remove::();
+ commands.entity(root_entity).despawn_recursive();
+
+ blueprint_events.send(BlueprintEvent::Spawned {blueprint_name:"".into(), blueprint_path: blueprint_path.0.clone() });
+
+ debug!("DONE WITH POST PROCESS");
+ }
+}
diff --git a/crates/blenvy/src/blender_settings.rs b/crates/blenvy/src/components/blender_settings.rs
similarity index 100%
rename from crates/blenvy/src/blender_settings.rs
rename to crates/blenvy/src/components/blender_settings.rs
diff --git a/crates/blenvy/src/blender_settings/lighting.rs b/crates/blenvy/src/components/blender_settings/lighting.rs
similarity index 100%
rename from crates/blenvy/src/blender_settings/lighting.rs
rename to crates/blenvy/src/components/blender_settings/lighting.rs
diff --git a/crates/blenvy/src/components/mod.rs b/crates/blenvy/src/components/mod.rs
new file mode 100644
index 0000000..e300b46
--- /dev/null
+++ b/crates/blenvy/src/components/mod.rs
@@ -0,0 +1,74 @@
+pub mod utils;
+pub use utils::*;
+
+pub mod ronstring_to_reflect_component;
+pub use ronstring_to_reflect_component::*;
+
+pub mod process_gltfs;
+pub use process_gltfs::*;
+
+pub mod blender_settings;
+
+use bevy::{
+ ecs::{component::Component, reflect::ReflectComponent, system::Resource},
+ prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update},
+ reflect::Reflect,
+};
+
+/// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities
+/// It will automatically run every time you load a gltf file
+/// Add this plugin to your Bevy app to get access to this feature
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy::gltf::*;
+/// # use bevy_gltf_components::ComponentsFromGltfPlugin;
+///
+/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/examples/basic for a real example
+/// fn main() {
+/// App::new()
+/// .add_plugins(DefaultPlugins)
+/// .add_plugin(ComponentsFromGltfPlugin)
+/// .add_system(spawn_level)
+/// .run();
+/// }
+///
+/// fn spawn_level(
+/// asset_server: Res,
+/// mut commands: bevy::prelude::Commands,
+/// keycode: Res>,
+
+/// ){
+/// if keycode.just_pressed(KeyCode::Return) {
+/// commands.spawn(SceneBundle {
+/// scene: asset_server.load("basic/models/level1.glb"),
+/// transform: Transform::from_xyz(2.0, 0.0, -5.0),
+/// ..Default::default()
+/// });
+/// }
+///}
+/// ```
+
+/// this is a flag component to tag a processed gltf, to avoid processing things multiple times
+#[derive(Component, Reflect, Default, Debug)]
+#[reflect(Component)]
+pub struct GltfProcessed;
+
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+/// systemset to order your systems after the component injection when needed
+pub enum GltfComponentsSet {
+ Injection,
+}
+
+#[derive(Default)]
+pub struct ComponentsFromGltfPlugin {}
+
+impl Plugin for ComponentsFromGltfPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugins(blender_settings::plugin)
+ .register_type::()
+ .add_systems(
+ Update,
+ (add_components_from_gltf_extras).in_set(GltfComponentsSet::Injection),
+ );
+ }
+}
diff --git a/crates/blenvy/src/process_gltfs.rs b/crates/blenvy/src/components/process_gltfs.rs
similarity index 62%
rename from crates/blenvy/src/process_gltfs.rs
rename to crates/blenvy/src/components/process_gltfs.rs
index 1afb3d3..73bd6be 100644
--- a/crates/blenvy/src/process_gltfs.rs
+++ b/crates/blenvy/src/components/process_gltfs.rs
@@ -15,7 +15,8 @@ use bevy::{
use crate::{ronstring_to_reflect_component, GltfProcessed};
-fn bla_balb(entity: Entity, name: &Name, parent: &Parent, reflect_components: Vec<(Box, TypeRegistration)>, mut entity_components: HashMap, TypeRegistration)>>){
+// , mut entity_components: HashMap, TypeRegistration)>>
+fn find_entity_components(entity: Entity, name: &Name, parent: &Parent, reflect_components: Vec<(Box, TypeRegistration)>, entity_components: &HashMap, TypeRegistration)>>) -> (Entity, Vec<(Box, TypeRegistration)>){
// we assign the components specified /xxx_components objects to their parent node
let mut target_entity = entity;
// if the node contains "components" or ends with "_pa" (ie add to parent), the components will not be added to the entity itself but to its parent
@@ -39,9 +40,11 @@ fn bla_balb(entity: Entity, name: &Name, parent: &Parent, reflect_components: Ve
for (component, type_registration) in reflect_components {
updated_components.push((component.clone_value(), type_registration));
}
- entity_components.insert(target_entity, updated_components);
+ return (target_entity, updated_components)
+ //entity_components.insert(target_entity, updated_components);
} else {
- entity_components.insert(target_entity, reflect_components);
+ return (target_entity, reflect_components);
+ // entity_components.insert(target_entity, reflect_components);
}
}
@@ -69,38 +72,54 @@ pub fn add_components_from_gltf_extras(world: &mut World) {
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
- bla_balb(entity, name, parent, reflect_components, entity_components);
-
- /*
- // we assign the components specified /xxx_components objects to their parent node
- let mut target_entity = entity;
- // if the node contains "components" or ends with "_pa" (ie add to parent), the components will not be added to the entity itself but to its parent
- // this is mostly used for Blender collections
- if name.as_str().contains("components") || name.as_str().ends_with("_pa") {
- debug!("adding components to parent");
- target_entity = parent.get();
- }
- debug!("adding to {:?}", target_entity);
-
- // if there where already components set to be added to this entity (for example when entity_data was refering to a parent), update the vec of entity_components accordingly
- // this allows for example blender collection to provide basic ecs data & the instances to override/ define their own values
- if entity_components.contains_key(&target_entity) {
- let mut updated_components: Vec<(Box, TypeRegistration)> = Vec::new();
- let current_components = &entity_components[&target_entity];
- // first inject the current components
- for (component, type_registration) in current_components {
- updated_components.push((component.clone_value(), type_registration.clone()));
- }
- // then inject the new components: this also enables overwrite components set in the collection
- for (component, type_registration) in reflect_components {
- updated_components.push((component.clone_value(), type_registration));
- }
- entity_components.insert(target_entity, updated_components);
- } else {
- entity_components.insert(target_entity, reflect_components);
- } */
+ let (target_entity, updated_components) = find_entity_components(entity, name, parent, reflect_components, &entity_components);
+ entity_components.insert(target_entity, updated_components);
}
+
+ for (entity, name, extra, parent) in scene_extras.iter(world) {
+ debug!(
+ "Name: {}, entity {:?}, parent: {:?}, scene_extras {:?}",
+ name, entity, parent, extra
+ );
+
+ let type_registry: &AppTypeRegistry = world.resource();
+ let type_registry = type_registry.read();
+ let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
+
+ let (target_entity, updated_components) = find_entity_components(entity, name, parent, reflect_components, &entity_components);
+ entity_components.insert(target_entity, updated_components);
+ }
+
+ for (entity, name, extra, parent) in mesh_extras.iter(world) {
+ debug!(
+ "Name: {}, entity {:?}, parent: {:?}, mesh_extras {:?}",
+ name, entity, parent, extra
+ );
+
+ let type_registry: &AppTypeRegistry = world.resource();
+ let type_registry = type_registry.read();
+ let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
+
+ let (target_entity, updated_components) = find_entity_components(entity, name, parent, reflect_components, &entity_components);
+ entity_components.insert(target_entity, updated_components);
+ }
+
+ for (entity, name, extra, parent) in material_extras.iter(world) {
+ debug!(
+ "Name: {}, entity {:?}, parent: {:?}, material_extras {:?}",
+ name, entity, parent, extra
+ );
+
+ let type_registry: &AppTypeRegistry = world.resource();
+ let type_registry = type_registry.read();
+ let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
+
+ let (target_entity, updated_components) = find_entity_components(entity, name, parent, reflect_components, &entity_components);
+ entity_components.insert(target_entity, updated_components);
+ }
+
+
for (entity, components) in entity_components {
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.clone();
diff --git a/crates/blenvy/src/ronstring_to_reflect_component.rs b/crates/blenvy/src/components/ronstring_to_reflect_component.rs
similarity index 100%
rename from crates/blenvy/src/ronstring_to_reflect_component.rs
rename to crates/blenvy/src/components/ronstring_to_reflect_component.rs
diff --git a/crates/blenvy/src/utils.rs b/crates/blenvy/src/components/utils.rs
similarity index 100%
rename from crates/blenvy/src/utils.rs
rename to crates/blenvy/src/components/utils.rs
diff --git a/crates/blenvy/src/lib.rs b/crates/blenvy/src/lib.rs
index 79af3d7..fc062be 100644
--- a/crates/blenvy/src/lib.rs
+++ b/crates/blenvy/src/lib.rs
@@ -1,78 +1,71 @@
-pub mod utils;
-pub use utils::*;
+use std::path::PathBuf;
+use bevy::{prelude::*, render::primitives::Aabb, utils::HashMap};
-pub mod ronstring_to_reflect_component;
-pub use ronstring_to_reflect_component::*;
+pub mod components;
+pub use components::*;
-pub mod process_gltfs;
-pub use process_gltfs::*;
+pub mod registry;
+pub use registry::*;
-pub mod blender_settings;
-
-use bevy::{
- ecs::{component::Component, reflect::ReflectComponent, system::Resource},
- prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update},
- reflect::Reflect,
-};
-
-/// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities
-/// It will automatically run every time you load a gltf file
-/// Add this plugin to your Bevy app to get access to this feature
-/// ```
-/// # use bevy::prelude::*;
-/// # use bevy::gltf::*;
-/// # use bevy_gltf_components::ComponentsFromGltfPlugin;
-///
-/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/examples/basic for a real example
-/// fn main() {
-/// App::new()
-/// .add_plugins(DefaultPlugins)
-/// .add_plugin(ComponentsFromGltfPlugin)
-/// .add_system(spawn_level)
-/// .run();
-/// }
-///
-/// fn spawn_level(
-/// asset_server: Res,
-/// mut commands: bevy::prelude::Commands,
-/// keycode: Res>,
-
-/// ){
-/// if keycode.just_pressed(KeyCode::Return) {
-/// commands.spawn(SceneBundle {
-/// scene: asset_server.load("basic/models/level1.glb"),
-/// transform: Transform::from_xyz(2.0, 0.0, -5.0),
-/// ..Default::default()
-/// });
-/// }
-///}
-/// ```
-
-/// this is a flag component to tag a processed gltf, to avoid processing things multiple times
-#[derive(Component, Reflect, Default, Debug)]
-#[reflect(Component)]
-pub struct GltfProcessed;
-
-#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
-/// systemset to order your systems after the component injection when needed
-pub enum GltfComponentsSet {
- Injection,
-}
+pub mod blueprints;
+pub use blueprints::*;
#[derive(Clone, Resource)]
-pub struct GltfComponentsConfig {}
+pub struct BlenvyConfig {
+ // registry
+ pub(crate) registry_save_path: PathBuf,
+ pub(crate) registry_component_filter: SceneFilter,
+ #[allow(dead_code)]
+ pub(crate) registry_resource_filter: SceneFilter,
-#[derive(Default)]
-pub struct ComponentsFromGltfPlugin {}
+ // blueprints
+ pub(crate) aabbs: bool,
+ pub(crate) aabb_cache: HashMap, // cache for aabbs
+ pub(crate) materials_cache: HashMap>, // cache for materials
+}
-impl Plugin for ComponentsFromGltfPlugin {
- fn build(&self, app: &mut App) {
- app.add_plugins(blender_settings::plugin)
- .register_type::()
- .insert_resource(GltfComponentsConfig {})
- .add_systems(
- Update,
- (add_components_from_gltf_extras).in_set(GltfComponentsSet::Injection),
- );
+
+#[derive(Debug, Clone)]
+/// Plugin for gltf blueprints
+pub struct BlenvyPlugin {
+ pub registry_save_path: PathBuf,
+
+ pub registry_component_filter: SceneFilter,
+ pub registry_resource_filter: SceneFilter,
+
+ /// Automatically generate aabbs for the blueprints root objects
+ pub aabbs: bool,
+}
+
+impl Default for BlenvyPlugin {
+ fn default() -> Self {
+ Self {
+ registry_save_path: PathBuf::from("registry.json"), // relative to assets folder
+ registry_component_filter: SceneFilter::default(),
+ registry_resource_filter: SceneFilter::default(),
+ aabbs: false,
+ }
}
}
+
+impl Plugin for BlenvyPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugins((
+ ComponentsFromGltfPlugin::default(),
+ ExportRegistryPlugin::default(),
+ BlueprintsPlugin::default()
+ ))
+ .insert_resource(BlenvyConfig {
+ registry_save_path: self.registry_save_path.clone(),
+ registry_component_filter: self.registry_component_filter.clone(),
+ registry_resource_filter: self.registry_resource_filter.clone(),
+
+ aabbs: self.aabbs,
+ aabb_cache: HashMap::new(),
+
+ materials_cache: HashMap::new(),
+ })
+ ;
+
+ }
+}
\ No newline at end of file
diff --git a/crates/blenvy/src/registry/export_types.rs b/crates/blenvy/src/registry/export_types.rs
new file mode 100644
index 0000000..eb0d833
--- /dev/null
+++ b/crates/blenvy/src/registry/export_types.rs
@@ -0,0 +1,273 @@
+use std::{fs::File, path::Path};
+use bevy::{log::info, prelude::{AppTypeRegistry, ReflectComponent, ReflectResource, World}, reflect::{TypeInfo, TypeRegistration, VariantInfo}};
+use serde_json::{json, Map, Value};
+use crate::{AssetRoot, BlenvyConfig};
+
+pub fn export_types(world: &mut World) {
+ let config = world
+ .get_resource::()
+ .expect("ExportComponentsConfig should exist at this stage");
+
+ let asset_root = world.resource::();
+ let registry_save_path = Path::join(&asset_root.0, &config.registry_save_path);
+ let writer = File::create(registry_save_path).expect("should have created schema file");
+
+ let components_to_filter_out = &config.registry_component_filter.clone();
+ let resources_to_filter_out = &config.registry_resource_filter.clone();
+
+ let types = world.resource_mut::();
+ let types = types.read();
+ let schemas = types
+ .iter()
+ .filter(|type_info| {
+ let type_id = type_info.type_id();
+ components_to_filter_out.is_allowed_by_id(type_id)
+ && resources_to_filter_out.is_allowed_by_id(type_id)
+ })
+ .map(export_type)
+ .collect::