Compare commits

...

3 Commits

Author SHA1 Message Date
Jan Hohenheim
2cdac0dddf
Merge 04a8ebcd4a into 9f21df035b 2024-03-18 12:15:50 -06:00
Mark Moissette
9f21df035b
feat(): Asset preloading basics (#169)
* closes #80 
* preliminary work for further asset management from the Blender side without needing additional dependencies & boilerplate on the Bevy side 
* feat(auto_export): 
     * now injecting list/tree of sub blueprints to main scenes/levels & blueprints
     * made blueprints/asset list conditional on NOT legacy mode
* feat(gltf_blueprints): 
    * basics of dynamic loading of sub_blueprints
    * added same kind of logic to the loading of material files
    * also made a more generic variant of the BlueprintAssetLoadTracker 
    * added gltf file path for more informative error in case a gltf is missing
* refactor(): 
   * made the various gltf assets loaded by bevy_asset_loader optional (to handle the case where they are not present)
   * updated all examples accordingly
* chore(crates): fixed links, slight tweaks, bumped versions
2024-03-18 18:00:19 +01:00
Mark Moissette
1353e14802
feat(bevy_components): qol improvements (#164)
* closes #163 
* closes #153 
* closes #154 
* feat(bevy_components): added tools for diagnostics/ finding & replacing invalid & unregistered components
   * added ui for listing invalid & unregistered components
   * added boilerplate & functionality for component renaming/replacing
   * injection of invalid status & message in case the conversion did not work well
   * added deletion of components individual & bulk
   * added handling of wrong string for unit structs : allows detection of more wrong values for components
   * added progress bars for bulk operators
   * added docs for new features
   * added tests
   * added small "attempt to fix" button for unit struct uis in case they are invalid
* feat(bevy_components): added progress indicators for from/to custom properties
* various other minor ui tweaks for workflow improvement
2024-03-07 16:29:04 +01:00
50 changed files with 1278 additions and 170 deletions

View File

@ -1,8 +1,8 @@
[package] [package]
name = "bevy_gltf_blueprints" name = "bevy_gltf_blueprints"
version = "0.9.0" version = "0.10.0"
authors = ["Mark 'kaosat-dev' Moissette"] authors = ["Mark 'kaosat-dev' Moissette"]
description = "Adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy." description = "Adds the ability to define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "gltf", "blueprint", "prefab"] keywords = ["gamedev", "bevy", "gltf", "blueprint", "prefab"]

View File

@ -5,7 +5,7 @@
# bevy_gltf_blueprints # bevy_gltf_blueprints
Built upon [bevy_gltf_components](https://crates.io/crates/bevy_gltf_components) this crate adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy. Built on [bevy_gltf_components](https://crates.io/crates/bevy_gltf_components) 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 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 ! * Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
@ -15,7 +15,9 @@ A blueprint is a set of **overrideable** components + a hierarchy: ie
* just a Gltf file with Gltf_extras specifying components * just a Gltf file with Gltf_extras specifying components
* a component called BlueprintName * 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 plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export) that does a lot of the work for you 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-ons that do a lot of the work for you
- [gltf_auto_export](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
- [bevy_components](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components)
## Usage ## Usage
@ -26,7 +28,7 @@ Here's a minimal usage example:
# Cargo.toml # Cargo.toml
[dependencies] [dependencies]
bevy="0.13" bevy="0.13"
bevy_gltf_blueprints = { version = "0.9"} bevy_gltf_blueprints = { version = "0.10"}
``` ```
@ -64,7 +66,7 @@ fn spawn_blueprint(
Add the following to your `[dependencies]` section in `Cargo.toml`: Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml ```toml
bevy_gltf_blueprints = "0.9" bevy_gltf_blueprints = "0.10"
``` ```
Or use `cargo add`: Or use `cargo add`:
@ -165,13 +167,10 @@ commands.spawn((
### BluePrintBundle ### BluePrintBundle
There is also a bundle for convenience , which just has There is also a ```BluePrintBundle``` for convenience , which just has
* a ```BlueprintName``` component * a ```BlueprintName``` component
* a ```SpawnHere``` component * a ```SpawnHere``` component
[```BluePrintBundle```](./src/lib.rs#22)
## Additional information ## Additional information
- When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert - When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert
@ -219,7 +218,7 @@ 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, 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 ```bevy_gltf_blueprints``` provides a **SystemSet** for that purpose:[```GltfBlueprintsSet```](./src/lib.rs#16) so ```bevy_gltf_blueprints``` provides a **SystemSet** for that purpose: ```GltfBlueprintsSet```
Typically , the order of systems should be Typically , the order of systems should be
@ -281,8 +280,7 @@ pub fn animation_change_on_proximity_foxes(
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation for how to set it up correctly see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation for how to set it up correctly
particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation/game/in_game.rs#86 particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation/game/in_game.rs
onward
## Materials ## Materials
@ -346,7 +344,7 @@ The main branch is compatible with the latest Bevy release, while the branch `be
Compatibility of `bevy_gltf_blueprints` versions: Compatibility of `bevy_gltf_blueprints` versions:
| `bevy_gltf_blueprints` | `bevy` | | `bevy_gltf_blueprints` | `bevy` |
| :-- | :-- | | :-- | :-- |
| `0.9` | `0.13` | | `0.9 - 0.10` | `0.13` |
| `0.3 - 0.8` | `0.12` | | `0.3 - 0.8` | `0.12` |
| `0.1 - 0.2` | `0.11` | | `0.1 - 0.2` | `0.11` |
| branch `main` | `0.13` | | branch `main` | `0.13` |

View File

@ -124,6 +124,9 @@ impl Plugin for BlueprintsPlugin {
.register_type::<MaterialInfo>() .register_type::<MaterialInfo>()
.register_type::<SpawnHere>() .register_type::<SpawnHere>()
.register_type::<Animations>() .register_type::<Animations>()
.register_type::<BlueprintsList>()
.register_type::<Vec<String>>()
.register_type::<HashMap<String, Vec<String>>>()
.insert_resource(BluePrintsConfig { .insert_resource(BluePrintsConfig {
format: self.format, format: self.format,
library_folder: self.library_folder.clone(), library_folder: self.library_folder.clone(),
@ -144,11 +147,24 @@ impl Plugin for BlueprintsPlugin {
.add_systems( .add_systems(
Update, Update,
( (
(
prepare_blueprints,
check_for_loaded,
spawn_from_blueprints, spawn_from_blueprints,
compute_scene_aabbs.run_if(aabbs_enabled),
apply_deferred.run_if(aabbs_enabled),
apply_deferred, apply_deferred,
materials_inject.run_if(materials_library_enabled), )
.chain(),
(compute_scene_aabbs, apply_deferred)
.chain()
.run_if(aabbs_enabled),
apply_deferred,
(
materials_inject,
check_for_material_loaded,
materials_inject2,
)
.chain()
.run_if(materials_library_enabled),
) )
.chain() .chain()
.in_set(GltfBlueprintsSet::Spawn), .in_set(GltfBlueprintsSet::Spawn),

View File

@ -4,6 +4,7 @@ use bevy::{
asset::{AssetServer, Assets, Handle}, asset::{AssetServer, Assets, Handle},
ecs::{ ecs::{
component::Component, component::Component,
entity::Entity,
query::{Added, With}, query::{Added, With},
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Commands, Query, Res, ResMut}, system::{Commands, Query, Res, ResMut},
@ -16,7 +17,7 @@ use bevy::{
render::mesh::Mesh, render::mesh::Mesh,
}; };
use crate::BluePrintsConfig; use crate::{AssetLoadTracker, AssetsToLoad, BluePrintsConfig};
#[derive(Component, Reflect, Default, Debug)] #[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)] #[reflect(Component)]
@ -26,10 +27,111 @@ pub struct MaterialInfo {
pub source: String, pub source: 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 /// system that injects / replaces materials from material library
pub(crate) fn materials_inject( pub(crate) fn materials_inject(
blueprints_config: ResMut<BluePrintsConfig>,
material_infos: Query<(Entity, &MaterialInfo), Added<MaterialInfo>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, material_info) in material_infos.iter() {
let model_file_name = format!(
"{}_materials_library.{}",
&material_info.source, &blueprints_config.format
);
let materials_path = Path::new(&blueprints_config.material_library_folder)
.join(Path::new(model_file_name.as_str()));
let material_name = &material_info.name;
let material_full_path = materials_path.to_str().unwrap().to_string() + "#" + material_name; // TODO: yikes, cleanup
if blueprints_config
.material_library_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
blueprints_config
.material_library_cache
.get(&material_full_path)
.expect("we should have the material available");
commands
.entity(entity)
.insert(BlueprintMaterialAssetsLoaded);
} else {
let material_file_handle: Handle<Gltf> = asset_server.load(materials_path.clone());
let material_file_id = material_file_handle.id();
let asset_infos: Vec<AssetLoadTracker<Gltf>> = vec![AssetLoadTracker {
name: material_full_path,
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 check_for_loaded, make generic ?
pub(crate) fn check_for_material_loaded(
mut blueprint_assets_to_load: Query<
(Entity, &mut AssetsToLoad<Gltf>),
With<BlueprintMaterialAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
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::<BlueprintMaterialAssetsNotLoaded>();
}
}
}
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject2(
mut blueprints_config: ResMut<BluePrintsConfig>, mut blueprints_config: ResMut<BluePrintsConfig>,
material_infos: Query<(&MaterialInfo, &Children), Added<MaterialInfo>>, material_infos: Query<
(&MaterialInfo, &Children),
(
Added<BlueprintMaterialAssetsLoaded>,
With<BlueprintMaterialAssetsLoaded>,
),
>,
with_materials_and_meshes: Query< with_materials_and_meshes: Query<
(), (),
( (
@ -38,9 +140,9 @@ pub(crate) fn materials_inject(
With<Handle<Mesh>>, With<Handle<Mesh>>,
), ),
>, >,
models: Res<Assets<bevy::gltf::Gltf>>, assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut commands: Commands, mut commands: Commands,
) { ) {
for (material_info, children) in material_infos.iter() { for (material_info, children) in material_infos.iter() {
@ -66,9 +168,9 @@ pub(crate) fn materials_inject(
.expect("we should have the material available"); .expect("we should have the material available");
material_found = Some(material); material_found = Some(material);
} else { } else {
let my_gltf: Handle<Gltf> = asset_server.load(materials_path.clone()); let model_handle: Handle<Gltf> = asset_server.load(materials_path.clone()); // FIXME: kinda weird now
let mat_gltf = models let mat_gltf = assets_gltf
.get(my_gltf.id()) .get(model_handle.id())
.expect("material should have been preloaded"); .expect("material should have been preloaded");
if mat_gltf.named_materials.contains_key(material_name) { if mat_gltf.named_materials.contains_key(material_name) {
let material = mat_gltf let material = mat_gltf

View File

@ -1,6 +1,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use bevy::{gltf::Gltf, prelude::*}; use bevy::{gltf::Gltf, prelude::*, utils::HashMap};
use crate::{Animations, BluePrintsConfig}; use crate::{Animations, BluePrintsConfig};
@ -46,8 +46,152 @@ pub struct AddToGameWorld;
/// helper component, just to transfer child data /// helper component, just to transfer child data
pub(crate) struct OriginalChildren(pub Vec<Entity>); pub(crate) struct OriginalChildren(pub Vec<Entity>);
/// main spawning functions, /// 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 BlueprintsList(pub HashMap<String, Vec<String>>);
/// helper component, for tracking loaded assets's loading state, id , handle etc
#[derive(Default, Debug)]
pub(crate) struct AssetLoadTracker<T: bevy::prelude::Asset> {
#[allow(dead_code)]
pub name: String,
pub id: AssetId<T>,
pub loaded: bool,
#[allow(dead_code)]
pub handle: Handle<T>,
}
/// helper component, for tracking loaded assets
#[derive(Component, Debug)]
pub(crate) struct AssetsToLoad<T: bevy::prelude::Asset> {
pub all_loaded: bool,
pub asset_infos: Vec<AssetLoadTracker<T>>,
pub progress: f32,
}
impl<T: bevy::prelude::Asset> Default for AssetsToLoad<T> {
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 /// * 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<BlueprintName>, Added<SpawnHere>, Without<Spawned>),
>,
mut commands: Commands,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>,
) {
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<AssetLoadTracker<Gltf>> = vec![];
let library_path =
library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0);
for (blueprint_name, _) in blueprints_list.0.iter() {
let model_file_name = format!("{}.{}", &blueprint_name, &blueprints_config.format);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
let model_handle: Handle<Gltf> = 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 check_for_loaded(
mut blueprint_assets_to_load: Query<
(Entity, &mut AssetsToLoad<Gltf>),
With<BlueprintAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
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::<BlueprintAssetsNotLoaded>();
}
}
}
pub(crate) fn spawn_from_blueprints( pub(crate) fn spawn_from_blueprints(
spawn_placeholders: Query< spawn_placeholders: Query<
( (
@ -59,7 +203,11 @@ pub(crate) fn spawn_from_blueprints(
Option<&AddToGameWorld>, Option<&AddToGameWorld>,
Option<&Name>, Option<&Name>,
), ),
(Added<BlueprintName>, Added<SpawnHere>, Without<Spawned>), (
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>, >,
mut commands: Commands, mut commands: Commands,
@ -82,17 +230,10 @@ pub(crate) fn spawn_from_blueprints(
) in spawn_placeholders.iter() ) in spawn_placeholders.iter()
{ {
debug!( debug!(
"need to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}", "attempting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent blupeprint_name.0, name, entity, original_parent
); );
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
let what = &blupeprint_name.0; let what = &blupeprint_name.0;
let model_file_name = format!("{}.{}", &what, &blueprints_config.format); let model_file_name = format!("{}.{}", &what, &blueprints_config.format);
@ -101,12 +242,15 @@ pub(crate) fn spawn_from_blueprints(
library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0); library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str())); let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
debug!("attempting to spawn {:?}", model_path); // info!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(model_path); let model_handle: Handle<Gltf> = asset_server.load(model_path.clone()); // FIXME: kinda weird now
let gltf = assets_gltf let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
.get(&model_handle) panic!(
.expect("this gltf should have been loaded"); "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 // 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 let main_scene_name = gltf
@ -123,6 +267,12 @@ pub(crate) fn spawn_from_blueprints(
transforms = *transform.unwrap(); transforms = *transform.unwrap();
} }
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
commands.entity(entity).insert(( commands.entity(entity).insert((
SceneBundle { SceneBundle {
scene: scene.clone(), scene: scene.clone(),

View File

@ -1,12 +1,16 @@
use std::any::TypeId; use std::any::TypeId;
use bevy::gltf::Gltf;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::scene::SceneInstance; use bevy::scene::SceneInstance;
// use bevy::utils::hashbrown::HashSet; // use bevy::utils::hashbrown::HashSet;
use super::{AnimationPlayerLink, Animations}; use super::{AnimationPlayerLink, Animations};
use super::{SpawnHere, Spawned}; use super::{SpawnHere, Spawned};
use crate::{CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren}; use crate::{
AssetsToLoad, BlueprintAssetsLoaded, CopyComponents, InBlueprint, NoInBlueprint,
OriginalChildren,
};
/// this system is in charge of doing any necessary post processing after a blueprint scene has been spawned /// 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 removes one level of useless nesting
@ -89,6 +93,8 @@ pub(crate) fn spawned_blueprint_post_process(
commands.entity(original).remove::<SpawnHere>(); commands.entity(original).remove::<SpawnHere>();
commands.entity(original).remove::<Spawned>(); commands.entity(original).remove::<Spawned>();
commands.entity(original).remove::<Handle<Scene>>(); commands.entity(original).remove::<Handle<Scene>>();
commands.entity(original).remove::<AssetsToLoad<Gltf>>(); // 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::<BlueprintAssetsLoaded>();
commands.entity(root_entity).despawn_recursive(); commands.entity(root_entity).despawn_recursive();
} }
} }

View File

@ -1,8 +1,8 @@
[package] [package]
name = "bevy_gltf_components" name = "bevy_gltf_components"
version = "0.5.0" version = "0.5.1"
authors = ["Mark 'kaosat-dev' Moissette"] authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side." 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" homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "assets", "gltf", "components"] keywords = ["gamedev", "bevy", "assets", "gltf", "components"]

View File

@ -13,9 +13,9 @@ This crate allows you to define [Bevy](https://bevyengine.org/) components direc
***important*** : the plugin for processing gltf files runs in ***update*** , so you cannot use the components directly if you spawn your scene from gltf in ***setup*** (the additional components will not show up) ***important*** : the plugin for processing gltf files runs in ***update*** , so you cannot use the components directly if you spawn your scene from gltf in ***setup*** (the additional components will not show up)
Please see the Please see the
* [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/examples/basic) * [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_components/basic)
* or use [```bevy_asset_loader```](https://github.com/NiklasEi/bevy_asset_loader) for a reliable workflow. * or use [```bevy_asset_loader```](https://github.com/NiklasEi/bevy_asset_loader) for reliable preloading of files, as this crate does not deal with loading your assets.
* alternatively, use the [```bevy_gltf_blueprints```](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/bevy_gltf_blueprints) crate, build on this crate's features, * alternatively, use the [```bevy_gltf_blueprints```](https://crates.io/crates/bevy_gltf_blueprints) crate, built on this crate's features,
that allows you to directly spawn entities from gltf based blueprints. that allows you to directly spawn entities from gltf based blueprints.
Here's a minimal usage example: Here's a minimal usage example:
@ -29,7 +29,7 @@ bevy_gltf_components = { version = "0.5"}
``` ```
```rust no_run ```rust no_run
//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 //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/bevy_gltf_components/examples/basic for a real example
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
@ -84,7 +84,7 @@ Or disable the legacy mode: (enabled by default)
ComponentsFromGltfPlugin{legacy_mode: false} ComponentsFromGltfPlugin{legacy_mode: false}
``` ```
You **need** to disable legacy mode if you want to use the [```bevy_components```](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/tools_bevy_blueprints/tools/bevy_components) Blender addon + the [```bevy_registry_export crate```](https://crates.io/crates/bevy_registry_export) ! You **need** to disable legacy mode if you want to use the [```bevy_components```](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components) Blender addon + the [```bevy_registry_export crate```](https://crates.io/crates/bevy_registry_export) !
As it create custom properties that are writen in real **ron** file format As it create custom properties that are writen in real **ron** file format
instead of a simplified version (the one in the legacy mode) instead of a simplified version (the one in the legacy mode)
@ -98,7 +98,7 @@ For example to replace your proxy components (stand-in components when you canno
which should happen **AFTER** the components from the gltf files have been injected, which should happen **AFTER** the components from the gltf files have been injected,
so ```bevy_gltf_components``` provides a **SystemSet** for that purpose:[```GltfComponentsSet```](./src/lib.rs#46) so ```bevy_gltf_components``` provides a **SystemSet** for that purpose:```GltfComponentsSet```
Typically , the order of systems should be Typically , the order of systems should be
@ -116,7 +116,7 @@ Typically , the order of systems should be
## Examples ## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_components/basic

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bevy_gltf_save_load" name = "bevy_gltf_save_load"
version = "0.4.0" version = "0.4.1"
authors = ["Mark 'kaosat-dev' Moissette"] authors = ["Mark 'kaosat-dev' Moissette"]
description = "Save & load your bevy games" description = "Save & load your bevy games"
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
@ -15,8 +15,7 @@ workspace = true
[dependencies] [dependencies]
bevy = { version = "0.13", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] } bevy = { version = "0.13", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
#bevy_gltf_blueprints = "0.9" bevy_gltf_blueprints = { version = "0.10", path = "../bevy_gltf_blueprints" }
bevy_gltf_blueprints = { version = "0.9", path = "../bevy_gltf_blueprints" }
[dev-dependencies] [dev-dependencies]
bevy = { version = "0.13", default-features = false, features = ["dynamic_linking"] } bevy = { version = "0.13", default-features = false, features = ["dynamic_linking"] }

View File

@ -36,7 +36,7 @@ Here's a minimal usage example:
[dependencies] [dependencies]
bevy="0.13" bevy="0.13"
bevy_gltf_save_load = "0.4" bevy_gltf_save_load = "0.4"
bevy_gltf_blueprints = "0.9" // also needed bevy_gltf_blueprints = "0.10" // also needed
``` ```
```rust no_run ```rust no_run
@ -133,7 +133,7 @@ pub fn setup_game(
``` ```
take a look at the [example]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for more clarity take a look at the [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for more clarity
## Installation ## Installation
@ -142,12 +142,7 @@ Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml ```toml
bevy_gltf_save_load = "0.3" bevy_gltf_save_load = "0.3"
<<<<<<< HEAD bevy_gltf_blueprints = "0.10" // also needed, as bevy_gltf_save_load does not re-export it at this time
bevy_gltf_blueprints = "0.8" // also needed, as bevy_gltf_save_load does not re-export it at this time
=======
bevy_gltf_blueprints = "0.6" // also needed, as bevy_gltf_save_load does not re-export it at this time
>>>>>>> 9cb9dda5d35c635d367fa81ca1a6c752cda9bc02
``` ```
Or use `cargo add`: Or use `cargo add`:
@ -268,7 +263,7 @@ pub fn request_load(
- ```LoadingFinished``` for loading - ```LoadingFinished``` for loading
> Note: I **highly** recomend you change states when you start/finish saving & loading, otherwise things **will** get unpredictable > Note: I **highly** recomend you change states when you start/finish saving & loading, otherwise things **will** get unpredictable
Please see [the example]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs#77') for this. Please see [the example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for this.
## Additional notes ## Additional notes
@ -287,8 +282,8 @@ For convenience ```bevy_gltf_save_load``` provides two **SystemSets**
Highly advised to get a better understanding of how things work ! Highly advised to get a better understanding of how things work !
To get started I recomend looking at To get started I recomend looking at
- [world setup]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/in_game.rs#13') - [world setup](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/in_game.rs)
- [various events & co]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs#77') - [various events & co](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs)
All examples are here: All examples are here:

View File

@ -1,11 +1,11 @@
[package] [package]
name = "bevy_registry_export" name = "bevy_registry_export"
version = "0.3.0" version = "0.3.1"
authors = ["Mark 'kaosat-dev' Moissette", "Pascal 'Killercup' Hertleif"] authors = ["Mark 'kaosat-dev' Moissette", "Pascal 'Killercup' Hertleif"]
description = "Allows you to define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side." description = "Allows you to create a Json export of all your components/ registered types of your Bevy app/game"
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "assets", "gltf", "components"] keywords = ["gamedev", "bevy", "assets", "registry", "components"]
categories = ["game-development"] categories = ["game-development"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@ -36,7 +36,7 @@ fn main() {
``` ```
take a look at the [example]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/basic/src/core/mod.rs) for more clarity take a look at the [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/basic/src/core/mod.rs) for more clarity
## Installation ## Installation

View File

@ -30,7 +30,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -20,7 +20,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -20,7 +20,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -21,7 +21,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -21,7 +21,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -76,7 +76,7 @@ pub fn trigger_level_transition(
} else if target_level == "Level2" { } else if target_level == "Level2" {
level = game_assets.level2.clone().unwrap(); level = game_assets.level2.clone().unwrap();
} else { } else {
level = game_assets.world.clone(); level = game_assets.world.clone().unwrap();
} }
info!("spawning new level"); info!("spawning new level");
commands.spawn(( commands.spawn((

View File

@ -21,7 +21,7 @@ pub fn setup_game(
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models scene: models
.get(game_assets.world.id()) .get(game_assets.world.clone().unwrap().id())
.expect("main level should have been loaded") .expect("main level should have been loaded")
.scenes[0] .scenes[0]
.clone(), .clone(),

View File

@ -5,8 +5,8 @@ use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)] #[derive(AssetCollection, Resource)]
pub struct GameAssets { pub struct GameAssets {
#[asset(key = "world")] #[asset(key = "world", optional)]
pub world: Handle<Gltf>, pub world: Option<Handle<Gltf>>,
#[asset(key = "world_dynamic", optional)] #[asset(key = "world_dynamic", optional)]
pub world_dynamic: Option<Handle<Gltf>>, pub world_dynamic: Option<Handle<Gltf>>,
@ -16,8 +16,8 @@ pub struct GameAssets {
#[asset(key = "level2", optional)] #[asset(key = "level2", optional)]
pub level2: Option<Handle<Gltf>>, pub level2: Option<Handle<Gltf>>,
#[asset(key = "models", collection(typed, mapped))] #[asset(key = "models", collection(typed, mapped), optional)]
pub models: HashMap<String, Handle<Gltf>>, pub models: Option<HashMap<String, Handle<Gltf>>>,
#[asset(key = "materials", collection(typed, mapped), optional)] #[asset(key = "materials", collection(typed, mapped), optional)]
pub materials: Option<HashMap<String, Handle<Gltf>>>, pub materials: Option<HashMap<String, Handle<Gltf>>>,

View File

@ -1,6 +1,6 @@
({ ({
"world":File (path: "models/World.glb"), /*"world":File (path: "models/World.glb"),
"models": Folder ( "models": Folder (
path: "models/library", path: "models/library",
), ),*/
}) })

View File

@ -3576,6 +3576,22 @@
"type": "array", "type": "array",
"typeInfo": "TupleStruct" "typeInfo": "TupleStruct"
}, },
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList": {
"isComponent": true,
"isResource": false,
"items": false,
"prefixItems": [
{
"type": {
"$ref": "#/$defs/bevy_utils::hashbrown::HashMap<alloc::string::String, alloc::vec::Vec<alloc::string::String>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>"
}
}
],
"short_name": "BlueprintsList",
"title": "bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList",
"type": "array",
"typeInfo": "TupleStruct"
},
"bevy_gltf_blueprints::spawn_from_blueprints::SpawnHere": { "bevy_gltf_blueprints::spawn_from_blueprints::SpawnHere": {
"additionalProperties": false, "additionalProperties": false,
"isComponent": true, "isComponent": true,
@ -10834,6 +10850,19 @@
"type": "object", "type": "object",
"typeInfo": "Value" "typeInfo": "Value"
}, },
"bevy_utils::hashbrown::HashMap<alloc::string::String, alloc::vec::Vec<alloc::string::String>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>": {
"additionalProperties": {
"type": {
"$ref": "#/$defs/alloc::vec::Vec<alloc::string::String>"
}
},
"isComponent": false,
"isResource": false,
"short_name": "HashMap<String, Vec<String>, DefaultHashBuilder>",
"title": "bevy_utils::hashbrown::HashMap<alloc::string::String, alloc::vec::Vec<alloc::string::String>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>",
"type": "object",
"typeInfo": "Map"
},
"bevy_utils::smallvec::SmallVec<[bevy_ecs::entity::Entity; 8]>": { "bevy_utils::smallvec::SmallVec<[bevy_ecs::entity::Entity; 8]>": {
"isComponent": false, "isComponent": false,
"isResource": false, "isResource": false,

View File

@ -11,6 +11,7 @@ impl Plugin for CorePlugin {
legacy_mode: false, legacy_mode: false,
library_folder: "models/library".into(), library_folder: "models/library".into(),
format: GltfFormat::GLB, format: GltfFormat::GLB,
material_library: true,
aabbs: true, aabbs: true,
..Default::default() ..Default::default()
}, },

View File

@ -1,37 +1,25 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag}; use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag};
use bevy_gltf_worlflow_examples_common_rapier::{assets::GameAssets, GameState, InAppRunning}; use bevy_gltf_worlflow_examples_common_rapier::{GameState, InAppRunning};
use bevy_rapier3d::prelude::Velocity; use bevy_rapier3d::prelude::Velocity;
use rand::Rng; use rand::Rng;
pub fn setup_game( pub fn setup_game(
mut commands: Commands, mut commands: Commands,
game_assets: Res<GameAssets>, asset_server: Res<AssetServer>,
models: Res<Assets<bevy::gltf::Gltf>>,
mut next_game_state: ResMut<NextState<GameState>>, mut next_game_state: ResMut<NextState<GameState>>,
) { ) {
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.2,
});
// here we actually spawn our game world/level // here we actually spawn our game world/level
commands.spawn(( commands.spawn((
SceneBundle { SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: asset_server.load("models/World.glb#Scene0"),
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default() ..default()
}, },
bevy::prelude::Name::from("world"), bevy::prelude::Name::from("world"),
GameWorldTag, GameWorldTag,
InAppRunning, InAppRunning,
)); ));
next_game_state.set(GameState::InGame) next_game_state.set(GameState::InGame)
} }

View File

@ -4,7 +4,7 @@ use std::{
time::Duration, time::Duration,
}; };
use bevy_gltf_blueprints::{AnimationPlayerLink, BlueprintName}; use bevy_gltf_blueprints::{AnimationPlayerLink, BlueprintName, BlueprintsList};
pub use in_game::*; pub use in_game::*;
use bevy::{ use bevy::{
@ -22,7 +22,8 @@ fn start_game(mut next_app_state: ResMut<NextState<AppState>>) {
// if the export from Blender worked correctly, we should have animations (simplified here by using AnimationPlayerLink) // if the export from Blender worked correctly, we should have animations (simplified here by using AnimationPlayerLink)
// if the export from Blender worked correctly, we should have an Entity called "Cylinder" that has two components: UnitTest, TupleTestF32 // if the export from Blender worked correctly, we should have an Entity called "Cylinder" that has two components: UnitTest, TupleTestF32
// if the export from Blender worked correctly, we should have an Entity called "Blueprint4_nested" that has a child called "Blueprint3" that has a "BlueprintName" component with value Blueprint3 // if the export from Blender worked correctly, we should have an Entity called "Blueprint4_nested" that has a child called "Blueprint3" that has a "BlueprintName" component with value Blueprint3
// if the export from Blender worked correctly, we should have a blueprints_list
#[allow(clippy::too_many_arguments)]
fn validate_export( fn validate_export(
parents: Query<&Parent>, parents: Query<&Parent>,
children: Query<&Children>, children: Query<&Children>,
@ -31,6 +32,8 @@ fn validate_export(
animation_player_links: Query<(Entity, &AnimationPlayerLink)>, animation_player_links: Query<(Entity, &AnimationPlayerLink)>,
exported_cylinder: Query<(Entity, &Name, &UnitTest, &TupleTestF32)>, exported_cylinder: Query<(Entity, &Name, &UnitTest, &TupleTestF32)>,
empties_candidates: Query<(Entity, &Name, &GlobalTransform)>, empties_candidates: Query<(Entity, &Name, &GlobalTransform)>,
blueprints_list: Query<(Entity, &BlueprintsList)>,
) { ) {
let animations_found = !animation_player_links.is_empty(); let animations_found = !animation_player_links.is_empty();
@ -69,11 +72,13 @@ fn validate_export(
} }
} }
let blueprints_list_found = !blueprints_list.is_empty();
fs::write( fs::write(
"bevy_diagnostics.json", "bevy_diagnostics.json",
format!( format!(
"{{ \"animations\": {}, \"cylinder_found\": {} , \"nested_blueprint_found\": {}, \"empty_found\": {} }}", "{{ \"animations\": {}, \"cylinder_found\": {} , \"nested_blueprint_found\": {}, \"empty_found\": {}, \"blueprints_list_found\": {} }}",
animations_found, cylinder_found, nested_blueprint_found, empty_found animations_found, cylinder_found, nested_blueprint_found, empty_found, blueprints_list_found
), ),
) )
.expect("Unable to write file"); .expect("Unable to write file");
@ -97,9 +102,9 @@ impl Plugin for GamePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Update, (spawn_test).run_if(in_state(GameState::InGame))) app.add_systems(Update, (spawn_test).run_if(in_state(GameState::InGame)))
.add_systems(Update, validate_export) .add_systems(Update, validate_export)
.add_systems(Update, generate_screenshot.run_if(on_timer(Duration::from_secs_f32(0.2)))) // TODO: run once
.add_systems(OnEnter(AppState::MenuRunning), start_game) .add_systems(OnEnter(AppState::MenuRunning), start_game)
.add_systems(OnEnter(AppState::AppRunning), setup_game) .add_systems(OnEnter(AppState::AppRunning), setup_game)
.add_systems(Update, generate_screenshot.run_if(on_timer(Duration::from_secs_f32(0.2)))) // TODO: run once
.add_systems( .add_systems(
Update, Update,
exit_game.run_if(on_timer(Duration::from_secs_f32(0.5))), exit_game.run_if(on_timer(Duration::from_secs_f32(0.5))),

View File

@ -57,6 +57,14 @@ Before you can use the add-on you need to configure it
![configuration 3](./docs/configuration3.png) ![configuration 3](./docs/configuration3.png)
#### registry file polling
* by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly
* you can set the polling frequency or turn it off if you do not want auto-refresh
![registry file polling](./docs/registry_polling.png)
## Use ## Use
@ -173,8 +181,8 @@ It will add the component to the select object
![invalid component](./docs/invalid_components.png) ![invalid component](./docs/invalid_components.png)
> important ! ```gltf_auto_export``` currently has no way of filtering out components, so you need to delete invalid components like these before exporting > see [here](#invalidunregistered-type-renaming--conversion) for ways to convert invalid / unregistered components to other types.
this will be adress in the future
- if you are encountering this type of view: don't panic your component data is not gone ! It just means you need to reload the registry data by clicking on the relevant button - if you are encountering this type of view: don't panic your component data is not gone ! It just means you need to reload the registry data by clicking on the relevant button
@ -182,18 +190,65 @@ It will add the component to the select object
## advanced configuration ## Advanced Tools
### registry file polling In this section you will find various additional more advanced tooling
### Invalid/unregistered type renaming / conversion
If you have components that are
* invalid : ie some error was diagnosed
* unregistered: a custom property is present on the object, but there is no matching type in the registry
Here you will get an overview, of ALL invalid and unregistered components in your Blender project, so you can find them, rename/convert them,
or delete them, also in bulk
![component rename overview](./docs/component_rename_overview2.png)
* you can click on the button to select the object in your outliner (this also works across scenes, so you will be taken to the scene where the
given object is located)
![update custom properties](./docs/component_rename_object_select.png)
* by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly #### Single object component renaming/ conversion
* you can set the polling frequency or turn it off if you do not want auto-refresh
![registry file polling](./docs/registry_polling.png) - to rename/convert a single component for a single object:
* go to the row of the object you want to convert the component of
* in the dropdown menu, choose the target component
* click on the button with the magic wand to convert the component
![single rename](./docs/component_rename_single.png)
> the tool will attempt to automatically convert the source component, including the field names/values, if the target component has the same ones
If it fails to do the conversion, you will get an error message, and you will either have to change the custom property yourself, or you can simply
change the values in the UI, which will automatically generate the custom property value
- to delete a single component for a single object:
* go to the row of the object you want to remove the component from
* click on the button with the "x" to remove the component
![single delete](./docs/component_remove_single.png)
#### Bulk component renaming/ conversion
- use this method if you want to convert ALL components of a given type of ALL objects
* click on this button to pick your source component
![bulk convert remove](./docs/component_rename_remove_bulk.png)
* for conversion: in the dropdown menu, choose the target component & click apply to convert all matching components
* for deletion: clic on the "x" to remove all matching components
![bulk convert remove](./docs/component_rename_remove_bulk2.png)
### regenerate custom property values ### For conversion between custom properties & components & vice-versa
#### regenerate custom property values
- "update custom properties of current object" : will go over **all components** that you have defined for the **currently selected object**, and re-generate the - "update custom properties of current object" : will go over **all components** that you have defined for the **currently selected object**, and re-generate the
@ -212,7 +267,7 @@ It will add the component to the select object
You should also re-export your gltf files , otherwise you might run into issues You should also re-export your gltf files , otherwise you might run into issues
### regenerate UI values #### regenerate component/ UI values
- since v0.2, you have the option to regenerate (for the selected object or all objects, as above) to regenerate your UI values from the custom property values - since v0.2, you have the option to regenerate (for the selected object or all objects, as above) to regenerate your UI values from the custom property values

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "bevy_components", "name": "bevy_components",
"author": "kaosigh", "author": "kaosigh",
"version": (0, 4, 0), "version": (0, 4, 1),
"blender": (3, 4, 0), "blender": (3, 4, 0),
"location": "VIEW_3D", "location": "VIEW_3D",
"description": "UI to help create Bevy blueprints and components", "description": "UI to help create Bevy blueprints and components",
@ -16,11 +16,11 @@ from bpy.props import (StringProperty)
from .helpers import load_settings from .helpers import load_settings
from .blueprints import CreateBlueprintOperator from .blueprints import CreateBlueprintOperator
from .components.operators import CopyComponentOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, Toggle_ComponentVisibility from .components.operators import CopyComponentOperator, Fix_Component_Operator, OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, RenameHelper, Toggle_ComponentVisibility
from .registry.registry import ComponentsRegistry,MissingBevyType from .registry.registry import ComponentsRegistry,MissingBevyType
from .registry.operators import (COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, ReloadRegistryOperator, OT_OpenFilebrowser) from .registry.operators import (COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, OT_select_component_name_to_replace, OT_select_object, ReloadRegistryOperator, OT_OpenFilebrowser)
from .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List) from .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_AdvancedToolsPanel, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List)
from .components.metadata import (ComponentMetadata, ComponentsMeta, ensure_metadata_for_all_objects) from .components.metadata import (ComponentMetadata, ComponentsMeta, ensure_metadata_for_all_objects)
from .propGroups.prop_groups import (generate_propertyGroups_for_components) from .propGroups.prop_groups import (generate_propertyGroups_for_components)
@ -87,6 +87,10 @@ classes = [
CopyComponentOperator, CopyComponentOperator,
PasteComponentOperator, PasteComponentOperator,
RemoveComponentOperator, RemoveComponentOperator,
RemoveComponentFromAllObjectsOperator,
Fix_Component_Operator,
OT_rename_component,
RenameHelper,
GenerateComponent_From_custom_property_Operator, GenerateComponent_From_custom_property_Operator,
Toggle_ComponentVisibility, Toggle_ComponentVisibility,
@ -106,8 +110,12 @@ classes = [
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_select_object,
OT_select_component_name_to_replace,
BEVY_COMPONENTS_PT_MainPanel, BEVY_COMPONENTS_PT_MainPanel,
BEVY_COMPONENTS_PT_ComponentsPanel, BEVY_COMPONENTS_PT_ComponentsPanel,
BEVY_COMPONENTS_PT_AdvancedToolsPanel,
BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_Configuration,
MISSING_TYPES_UL_List, MISSING_TYPES_UL_List,
BEVY_COMPONENTS_PT_MissingTypesPanel, BEVY_COMPONENTS_PT_MissingTypesPanel,

View File

@ -237,6 +237,23 @@ def apply_propertyGroup_values_to_object_customProperties(object):
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None) value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value object[component_name] = value
# apply component value(s) to custom property of a single component
def apply_propertyGroup_values_to_object_customProperties_for_component(object, component_name):
registry = bpy.context.window_manager.components_registry
print("yallah", component_name)
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_short_name(component_name)
if component_definition != None:
print("merde")
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value
components_metadata = object.components_meta.components
componentMeta = next(filter(lambda component: component["name"] == component_name, components_metadata), None)
if componentMeta:
print("here")
componentMeta.invalid = False
componentMeta.invalid_details = ""
def apply_customProperty_values_to_object_propertyGroups(object): def apply_customProperty_values_to_object_propertyGroups(object):
@ -258,6 +275,8 @@ def apply_customProperty_values_to_object_propertyGroups(object):
object["__disable__update"] = True # disable update callback while we set the values of the propertyGroup "tree" (as a propertyGroup can contain other propertyGroups) object["__disable__update"] = True # disable update callback while we set the values of the propertyGroup "tree" (as a propertyGroup can contain other propertyGroups)
property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, customProperty_value) property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, customProperty_value)
del object["__disable__update"] del object["__disable__update"]
source_componentMeta.invalid = False
source_componentMeta.invalid_details = ""
# removes the given component from the object: removes both the custom property and the matching metadata from the object # removes the given component from the object: removes both the custom property and the matching metadata from the object
def remove_component_from_object(object, component_name): def remove_component_from_object(object, component_name):

View File

@ -3,7 +3,7 @@ import json
import bpy import bpy
from bpy_types import Operator from bpy_types import Operator
from bpy.props import (StringProperty) from bpy.props import (StringProperty)
from .metadata import add_component_to_object, add_metadata_to_components_without_metadata, apply_customProperty_values_to_object_propertyGroups, copy_propertyGroup_values_to_another_object, find_component_definition_from_short_name, remove_component_from_object from .metadata import add_component_to_object, add_metadata_to_components_without_metadata, apply_customProperty_values_to_object_propertyGroups, apply_propertyGroup_values_to_object_customProperties_for_component, copy_propertyGroup_values_to_another_object, find_component_definition_from_short_name, remove_component_from_object
class AddComponentOperator(Operator): class AddComponentOperator(Operator):
"""Add component to blueprint""" """Add component to blueprint"""
@ -90,12 +90,10 @@ class PasteComponentOperator(Operator):
return {'FINISHED'} return {'FINISHED'}
class RemoveComponentOperator(Operator): class RemoveComponentOperator(Operator):
"""Delete component from blueprint""" """Remove component from object"""
bl_idname = "object.remove_bevy_component" bl_idname = "object.remove_bevy_component"
bl_label = "Delete component from blueprint Operator" bl_label = "Remove component from object Operator"
bl_options = {"UNDO"} bl_options = {"UNDO"}
component_name: StringProperty( component_name: StringProperty(
@ -103,11 +101,18 @@ class RemoveComponentOperator(Operator):
description="component to delete", description="component to delete",
) # type: ignore ) # type: ignore
object_name: StringProperty(
name="object name",
description="object whose component to delete",
default=""
) # type: ignore
def execute(self, context): def execute(self, context):
if self.object_name == "":
object = context.object object = context.object
else:
object = bpy.data.objects[self.object_name]
print("removing component ", self.component_name, "from object '"+object.name+"'") print("removing component ", self.component_name, "from object '"+object.name+"'")
if object is not None and self.component_name in object: if object is not None and self.component_name in object:
remove_component_from_object(object, self.component_name) remove_component_from_object(object, self.component_name)
else: else:
@ -116,6 +121,154 @@ class RemoveComponentOperator(Operator):
return {'FINISHED'} return {'FINISHED'}
class RemoveComponentFromAllObjectsOperator(Operator):
"""Remove component from all object"""
bl_idname = "object.remove_bevy_component_all"
bl_label = "Remove component from all objects Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to delete",
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_remove_progress = bpy.props.FloatProperty(default=-1.0)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_remove_progress
def execute(self, context):
print("removing component ", self.component_name, "from all objects")
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
if len(object.keys()) > 0:
if object is not None and self.component_name in object:
remove_component_from_object(object, self.component_name)
progress = index / total
context.window_manager.components_remove_progress = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.components_remove_progress = -1.0
return {'FINISHED'}
class RenameHelper(bpy.types.PropertyGroup):
original_name: bpy.props.StringProperty(name="") # type: ignore
new_name: bpy.props.StringProperty(name="") # type: ignore
#object: bpy.props.PointerProperty(type=bpy.types.Object)
@classmethod
def register(cls):
bpy.types.WindowManager.bevy_component_rename_helper = bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
# remove handlers & co
del bpy.types.WindowManager.bevy_component_rename_helper
class OT_rename_component(Operator):
"""Rename component"""
bl_idname = "object.rename_bevy_component"
bl_label = "rename component"
bl_options = {"UNDO"}
original_name: bpy.props.StringProperty(default="") # type: ignore
new_name: StringProperty(
name="new_name",
description="new name of component",
) # type: ignore
target_objects: bpy.props.StringProperty() # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_rename_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_rename_progress
def execute(self, context):
registry = context.window_manager.components_registry
type_infos = registry.type_infos
settings = context.window_manager.bevy_component_rename_helper
original_name = settings.original_name if self.original_name == "" else self.original_name
new_name = self.new_name
print("renaming components: original name", original_name, "new_name", self.new_name, "targets", self.target_objects)
target_objects = json.loads(self.target_objects)
errors = []
total = len(target_objects)
if original_name != '' and new_name != '' and original_name != new_name and len(target_objects) > 0:
for index, object_name in enumerate(target_objects):
object = bpy.data.objects[object_name]
if object and original_name in object:
# copy data to new component, remove the old one
try:
object[new_name] = object[original_name]
remove_component_from_object(object, original_name)
except Exception as error:
if '__disable__update' in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
# get metadata
components_metadata = getattr(object, "components_meta", None)
if components_metadata:
components_metadata = components_metadata.components
component_meta = next(filter(lambda component: component["name"] == new_name, components_metadata), None)
if component_meta:
component_meta.invalid = True
component_meta.invalid_details = "unknow issue when renaming/transforming component, please remove it & add it back again"
errors.append( "failed to copy old component value to new component: object: '" + object.name + "', error: " + str(error))
try:
# attempt conversion
long_name = registry.short_names_to_long_names[new_name]
component_definition = type_infos[long_name]
add_component_to_object(object, component_definition, object[new_name])
except Exception as error:
if '__disable__update' in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
components_metadata = getattr(object, "components_meta", None)
if components_metadata:
components_metadata = components_metadata.components
component_meta = next(filter(lambda component: component["name"] == new_name, components_metadata), None)
if component_meta:
component_meta.invalid = True
component_meta.invalid_details = "wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate"
errors.append( "wrong custom property values to generate target component: object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_rename_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except: pass # this is to allow this to run in cli/headless mode
if len(errors) > 0:
self.report({'ERROR'}, "Failed to rename component: Errors:" + str(errors))
else:
self.report({'INFO'}, "Sucessfully renamed component")
#clear data after we are done
self.original_name = ""
context.window_manager.bevy_component_rename_helper.original_name = ""
context.window_manager.components_rename_progress = -1.0
return {'FINISHED'}
class GenerateComponent_From_custom_property_Operator(Operator): class GenerateComponent_From_custom_property_Operator(Operator):
"""generate components from custom property""" """generate components from custom property"""
bl_idname = "object.generate_bevy_component_from_custom_property" bl_idname = "object.generate_bevy_component_from_custom_property"
@ -143,6 +296,31 @@ class GenerateComponent_From_custom_property_Operator(Operator):
return {'FINISHED'} return {'FINISHED'}
class Fix_Component_Operator(Operator):
"""attempt to fix component"""
bl_idname = "object.fix_bevy_component"
bl_label = "Fix component (attempts to)"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to fix",
) # type: ignore
def execute(self, context):
object = context.object
error = False
try:
apply_propertyGroup_values_to_object_customProperties_for_component(object, self.component_name)
except Exception as error:
if "__disable__update" in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to fix component: Error:" + str(error))
if not error:
self.report({'INFO'}, "Sucessfully fixed component (please double check component & its custom property value)")
return {'FINISHED'}
class Toggle_ComponentVisibility(Operator): class Toggle_ComponentVisibility(Operator):
"""toggles components visibility""" """toggles components visibility"""
bl_idname = "object.toggle_bevy_component_visibility" bl_idname = "object.toggle_bevy_component_visibility"

View File

@ -1,7 +1,9 @@
import json import json
import bpy import bpy
from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT
from .metadata import do_object_custom_properties_have_missing_metadata from .metadata import do_object_custom_properties_have_missing_metadata
from .operators import AddComponentOperator, CopyComponentOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility from .operators import AddComponentOperator, CopyComponentOperator, Fix_Component_Operator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility
def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None): def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum") is_enum = getattr(propertyGroup, "with_enum")
@ -193,6 +195,16 @@ class BEVY_COMPONENTS_PT_ComponentsPanel(bpy.types.Panel):
row.label(text=error_message) row.label(text=error_message)
# "footer" with additional controls # "footer" with additional controls
if component_invalid:
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
unit_struct = len(propertyGroup.field_names) == 0
if unit_struct:
op = row.operator(Fix_Component_Operator.bl_idname, text="", icon="SHADERFX")
op.component_name = component_name
row.separator()
op = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X") op = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
op.component_name = component_name op.component_name = component_name
row.separator() row.separator()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -224,8 +224,9 @@ def property_group_value_from_custom_property_value(property_group, definition,
else: else:
pass if len(value) > 2: #a unit struct should be two chars long :()
#print("struct with zero fields") #print("struct with zero fields")
raise Exception("input string too big for a unit struct")
elif type_info == "Tuple": elif type_info == "Tuple":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1) custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1)

View File

@ -42,11 +42,26 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL(Operator):
bl_label = "Apply Registry to all objects" bl_label = "Apply Registry to all objects"
bl_options = {"UNDO"} bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress_all
def execute(self, context): def execute(self, context):
print("apply registry to all") print("apply registry to all")
#context.window_manager.components_registry.load_schema() #context.window_manager.components_registry.load_schema()
for object in bpy.data.objects: total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
apply_propertyGroup_values_to_object_customProperties(object) apply_propertyGroup_values_to_object_customProperties(object)
progress = index / total
context.window_manager.custom_properties_from_components_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.custom_properties_from_components_progress_all = -1.0
return {'FINISHED'} return {'FINISHED'}
@ -56,10 +71,23 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT(Operator):
bl_label = "Apply Registry to current object" bl_label = "Apply Registry to current object"
bl_options = {"UNDO"} bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress
def execute(self, context): def execute(self, context):
print("apply registry to current object") print("apply registry to current object")
object = context.object object = context.object
context.window_manager.custom_properties_from_components_progress = 0.5
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
apply_propertyGroup_values_to_object_customProperties(object) apply_propertyGroup_values_to_object_customProperties(object)
context.window_manager.custom_properties_from_components_progress = -1.0
return {'FINISHED'} return {'FINISHED'}
@ -69,44 +97,79 @@ class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT(Operator):
bl_label = "Apply custom_properties to current object" bl_label = "Apply custom_properties to current object"
bl_options = {"UNDO"} bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress
def execute(self, context): def execute(self, context):
print("apply custom properties to current object") print("apply custom properties to current object")
object = context.object object = context.object
error = False error = False
try: try:
apply_customProperty_values_to_object_propertyGroups(object) apply_customProperty_values_to_object_propertyGroups(object)
progress = 0.5
context.window_manager.components_from_custom_properties_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except:pass # ony run in ui
except Exception as error: except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error)) self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error))
if not error: if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object") self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object")
context.window_manager.components_from_custom_properties_progress = -1.0
return {'FINISHED'} return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator): class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator):
"""Update UI values from custom properties to ALL object""" """Update UI values from custom properties to ALL object"""
bl_idname = "object.refresh_ui_from_custom_properties_all" bl_idname = "object.refresh_ui_from_custom_properties_all"
bl_label = "Apply custom_properties to all objects" bl_label = "Apply custom_properties to all objects"
bl_options = {"UNDO"} bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress_all
def execute(self, context): def execute(self, context):
print("apply custom properties to all object") print("apply custom properties to all object")
bpy.context.window_manager.components_registry.disable_all_object_updates = True bpy.context.window_manager.components_registry.disable_all_object_updates = True
errors = [] errors = []
for object in bpy.data.objects: total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
try: try:
apply_customProperty_values_to_object_propertyGroups(object) apply_customProperty_values_to_object_propertyGroups(object)
except Exception as error: except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
errors.append( "object: '" + object.name + "', error: " + str(error)) errors.append( "object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_from_custom_properties_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
if len(errors) > 0: if len(errors) > 0:
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors)) self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors))
else: else:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects") self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects")
bpy.context.window_manager.components_registry.disable_all_object_updates = False bpy.context.window_manager.components_registry.disable_all_object_updates = False
context.window_manager.components_from_custom_properties_progress_all = -1.0
return {'FINISHED'} return {'FINISHED'}
class OT_OpenFilebrowser(Operator, ImportHelper): class OT_OpenFilebrowser(Operator, ImportHelper):
@ -133,3 +196,41 @@ class OT_OpenFilebrowser(Operator, ImportHelper):
return {'FINISHED'} return {'FINISHED'}
class OT_select_object(Operator):
"""Select object by name"""
bl_idname = "object.select"
bl_label = "Select object"
bl_options = {"UNDO"}
object_name: StringProperty(
name="object_name",
description="object to select's name ",
) # type: ignore
def execute(self, context):
if self.object_name:
object = bpy.data.objects[self.object_name]
scenes_of_object = list(object.users_scene)
if len(scenes_of_object) > 0:
bpy.ops.object.select_all(action='DESELECT')
bpy.context.window.scene = scenes_of_object[0]
object.select_set(True)
bpy.context.view_layer.objects.active = object
return {'FINISHED'}
class OT_select_component_name_to_replace(Operator):
"""Select component name to replace"""
bl_idname = "object.select_component_name_to_replace"
bl_label = "Select component name for bulk replace"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component_name",
description="component name to replace",
) # type: ignore
def execute(self, context):
context.window_manager.bevy_component_rename_helper.original_name = self.component_name
return {'FINISHED'}

View File

@ -241,9 +241,6 @@ class ComponentsRegistry(PropertyGroup):
del bpy.types.WindowManager.components_registry del bpy.types.WindowManager.components_registry
def load_schema(self): def load_schema(self):
print("load schema", self) print("load schema", self)
# cleanup previous data if any # cleanup previous data if any
@ -352,9 +349,9 @@ class ComponentsRegistry(PropertyGroup):
return propGroupName return propGroupName
def get_propertyGroupName_from_shortName(self, shortName): def get_propertyGroupName_from_shortName(self, shortName):
return self.short_names_to_propgroup_names.get(shortName, None) return self.short_names_to_propgroup_names.get(shortName, None)
###########
""" """
object[component_definition.name] = 0.5 object[component_definition.name] = 0.5

View File

@ -1,9 +1,15 @@
import json
import bpy import bpy
from bpy_types import (UIList) from bpy_types import (UIList)
from bpy.props import (StringProperty)
from ..components.operators import OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator
from .operators import( from .operators import(
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_OpenFilebrowser, ReloadRegistryOperator, OT_OpenFilebrowser,
OT_select_component_name_to_replace,
OT_select_object, ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT) COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT)
@ -21,8 +27,7 @@ class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
registry = context.window_manager.components_registry registry = context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
selected_object = context.selected_objects[0] if len(context.selected_objects) > 0 else None
row = layout.row() row = layout.row()
col = row.column() col = row.column()
@ -43,31 +48,212 @@ class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
layout.separator() layout.separator()
layout.separator() layout.separator()
class BEVY_COMPONENTS_PT_AdvancedToolsPanel(bpy.types.Panel):
"""panel listing all the missing bevy types in the schema"""
bl_idname = "BEVY_COMPONENTS_PT_AdvancedToolsPanel"
bl_label = "Advanced tools"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "advanced tooling"
def draw_invalid_or_unregistered_header(self, layout, items):
row = layout.row()
for item in items:
col = row.column()
col.label(text=item)
def draw_invalid_or_unregistered(self, layout, status, component_name, object):
available_components = bpy.context.window_manager.components_list
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
row = layout.row()
col = row.column()
col.label(text=component_name)
col = row.column()
operator = col.operator(OT_select_object.bl_idname, text=object.name)
operator.object_name = object.name
col = row.column()
col.label(text=status)
col = row.column()
col.prop(available_components, "list", text="")
col = row.column()
operator = col.operator(OT_rename_component.bl_idname, text="", icon="SHADERFX") #rename
new_name = registry.type_infos[available_components.list]['short_name'] if available_components.list in registry.type_infos else ""
operator.original_name = component_name
operator.target_objects = json.dumps([object.name])
operator.new_name = new_name
col.enabled = registry_has_type_infos and component_name != "" and component_name != new_name
col = row.column()
operator = col.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
operator.object_name = object.name
operator.component_name = component_name
col = row.column()
col = row.column()
operator = col.operator(OT_select_component_name_to_replace.bl_idname, text="", icon="EYEDROPPER") #text="select for rename",
operator.component_name = component_name
def draw(self, context):
layout = self.layout
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
selected_object = context.selected_objects[0] if len(context.selected_objects) > 0 else None
available_components = bpy.context.window_manager.components_list
row = layout.row()
box= row.box()
box.label(text="Invalid/ unregistered components")
objects_with_invalid_components = []
invalid_component_names = []
self.draw_invalid_or_unregistered_header(layout, ["Component", "Object", "Status", "Target"])
for object in bpy.data.objects: # TODO: very inneficent
if len(object.keys()) > 0:
if "components_meta" in object:
components_metadata = object.components_meta.components
comp_names = []
for index, component_meta in enumerate(components_metadata):
short_name = component_meta.name
if component_meta.invalid:
self.draw_invalid_or_unregistered(layout, "Invalid", short_name, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
if not short_name in invalid_component_names:
invalid_component_names.append(short_name)
comp_names.append(short_name)
for custom_property in object.keys():
if custom_property != 'components_meta' and custom_property not in comp_names:
self.draw_invalid_or_unregistered(layout, "Unregistered", custom_property, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
if not short_name in invalid_component_names:
invalid_component_names.append(custom_property)
layout.separator()
layout.separator()
original_name = bpy.context.window_manager.bevy_component_rename_helper.original_name
row = layout.row()
col = row.column()
col.label(text="Original")
col = row.column()
col.label(text="New")
col = row.column()
col.label(text="------")
row = layout.row()
col = row.column()
box = col.box()
box.label(text=original_name)
col = row.column()
col.prop(available_components, "list", text="")
#row.prop(available_components, "filter",text="Filter")
col = row.column()
components_rename_progress = context.window_manager.components_rename_progress
if components_rename_progress == -1.0:
operator = col.operator(OT_rename_component.bl_idname, text="apply", icon="SHADERFX")
operator.target_objects = json.dumps(objects_with_invalid_components)
new_name = registry.type_infos[available_components.list]['short_name'] if available_components.list in registry.type_infos else ""
operator.new_name = new_name
col.enabled = registry_has_type_infos and original_name != "" and original_name != new_name
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = components_rename_progress, text=f"updating {components_rename_progress * 100.0:.2f}%")
col = row.column()
remove_components_progress = context.window_manager.components_remove_progress
if remove_components_progress == -1.0:
operator = row.operator(RemoveComponentFromAllObjectsOperator.bl_idname, text="", icon="X")
operator.component_name = context.window_manager.bevy_component_rename_helper.original_name
col.enabled = registry_has_type_infos and original_name != ""
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = remove_components_progress, text=f"updating {remove_components_progress * 100.0:.2f}%")
layout.separator()
layout.separator()
row = layout.row()
box= row.box()
box.label(text="Conversions between custom properties and components & vice-versa")
row = layout.row() row = layout.row()
row.label(text="WARNING ! The following operations will overwrite your existing custom properties if they have matching types on the bevy side !") row.label(text="WARNING ! The following operations will overwrite your existing custom properties if they have matching types on the bevy side !")
row.alert = True row.alert = True
##
row = layout.row() row = layout.row()
custom_properties_from_components_progress_current = context.window_manager.custom_properties_from_components_progress
if custom_properties_from_components_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update custom properties of current object" , icon="LOOP_FORWARDS") row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update custom properties of current object" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos and selected_object is not None row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_current, text=f"updating {custom_properties_from_components_progress_current * 100.0:.2f}%")
layout.separator() layout.separator()
row = layout.row() row = layout.row()
custom_properties_from_components_progress_all = context.window_manager.custom_properties_from_components_progress_all
if custom_properties_from_components_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL.bl_idname, text="update custom properties of ALL objects" , icon="LOOP_FORWARDS") row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL.bl_idname, text="update custom properties of ALL objects" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_all, text=f"updating {custom_properties_from_components_progress_all * 100.0:.2f}%")
########################
row = layout.row() row = layout.row()
row.label(text="WARNING ! The following operations will try to overwrite your existing ui values if they have matching types on the bevy side !") row.label(text="WARNING ! The following operations will try to overwrite your existing ui values if they have matching types on the bevy side !")
row.alert = True row.alert = True
components_from_custom_properties_progress_current = context.window_manager.components_from_custom_properties_progress
row = layout.row() row = layout.row()
if components_from_custom_properties_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update UI FROM custom properties of current object" , icon="LOOP_BACK") row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update UI FROM custom properties of current object" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos and selected_object is not None row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_current, text=f"updating {components_from_custom_properties_progress_current * 100.0:.2f}%")
layout.separator() layout.separator()
row = layout.row() row = layout.row()
components_from_custom_properties_progress_all = context.window_manager.components_from_custom_properties_progress_all
if components_from_custom_properties_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL.bl_idname, text="update UI FROM custom properties of ALL objects" , icon="LOOP_BACK") row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL.bl_idname, text="update UI FROM custom properties of ALL objects" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_all, text=f"updating {components_from_custom_properties_progress_all * 100.0:.2f}%")
class BEVY_COMPONENTS_PT_MissingTypesPanel(bpy.types.Panel): class BEVY_COMPONENTS_PT_MissingTypesPanel(bpy.types.Panel):

View File

@ -15,6 +15,7 @@ expected_custom_property_values = {'AComponentWithAnExtremlyExageratedOrMaybeNot
'0.0, low_frequency_boost_curvature: 0.0, prefilter_settings: (threshold: 0.0, threshold_softness: ' '0.0, low_frequency_boost_curvature: 0.0, prefilter_settings: (threshold: 0.0, threshold_softness: '
'0.0))', '0.0))',
'BlueprintName': '(" ")', 'BlueprintName': '(" ")',
'BlueprintsList': '("")',
'BorderColor': '(Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))', 'BorderColor': '(Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))',
'Button': '()', 'Button': '()',
'CalculatedClip': '(clip: (max: Vec2(x:0.0, y:0.0), min: Vec2(x:0.0, y:0.0)))', 'CalculatedClip': '(clip: (max: Vec2(x:0.0, y:0.0), min: Vec2(x:0.0, y:0.0)))',
@ -214,6 +215,7 @@ expected_custom_property_values_randomized = {'AComponentWithAnExtremlyExagerate
'0.8133212327957153, prefilter_settings: (threshold: 0.8235888481140137, threshold_softness: ' '0.8133212327957153, prefilter_settings: (threshold: 0.8235888481140137, threshold_softness: '
'0.6534725427627563))', '0.6534725427627563))',
'BlueprintName': '("sbnpsago")', 'BlueprintName': '("sbnpsago")',
'BlueprintsList': '("")',
'BorderColor': '(Rgba(red:0.5714026093482971, green:0.42888906598091125, blue:0.5780913233757019, ' 'BorderColor': '(Rgba(red:0.5714026093482971, green:0.42888906598091125, blue:0.5780913233757019, '
'alpha:0.20609822869300842))', 'alpha:0.20609822869300842))',
'Button': '()', 'Button': '()',

View File

@ -48,12 +48,12 @@ def test_components_should_generate_correct_custom_properties(setup_data):
except Exception as error: except Exception as error:
errors.append(error) errors.append(error)
'''pp = pprint.PrettyPrinter(depth=14, width=120) pp = pprint.PrettyPrinter(depth=14, width=120)
print("CUSTOM PROPERTY VALUES") print("CUSTOM PROPERTY VALUES")
pp.pprint(custom_property_values)''' pp.pprint(custom_property_values)
assert len(errors) == 0 assert len(errors) == 0
assert len(added_components) == 158 assert len(added_components) == 159
def test_components_should_generate_correct_custom_properties_with_randomized_values(setup_data): def test_components_should_generate_correct_custom_properties_with_randomized_values(setup_data):
@ -105,7 +105,7 @@ def test_components_should_generate_correct_custom_properties_with_randomized_va
print("error_components", error_components) print("error_components", error_components)
assert len(errors) == 0 assert len(errors) == 0
assert len(added_components) == 158 assert len(added_components) == 159
def test_components_should_generate_correct_propertyGroup_values_from_custom_properties(setup_data): def test_components_should_generate_correct_propertyGroup_values_from_custom_properties(setup_data):
registry = bpy.context.window_manager.components_registry registry = bpy.context.window_manager.components_registry
@ -163,7 +163,7 @@ def test_components_should_generate_correct_propertyGroup_values_from_custom_pro
for index, error in enumerate(errors): for index, error in enumerate(errors):
print("ERROR", error, failing_components[index]) print("ERROR", error, failing_components[index])
assert len(errors) == 0 assert len(errors) == 0
assert len(added_components) == 158 assert len(added_components) == 159
def test_remove_components(setup_data): def test_remove_components(setup_data):

View File

@ -0,0 +1,159 @@
import json
import re
import bpy
import pprint
import pytest
from .setup_data import setup_data
# small helpers
def get_component_metadata(object, component_name):
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == component_name, target_components_metadata), None)
return component_meta
def get_component_propGroup(registry, component_name, component_meta):
# component_type = registry.short_names_to_long_names[component_name]
# add_component_operator = bpy.ops.object.add_bevy_component
property_group_name = registry.get_propertyGroupName_from_shortName(component_name)
propertyGroup = getattr(component_meta, property_group_name, None)
return propertyGroup
def test_rename_component_single_unit_struct(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
rename_component_operator = bpy.ops.object.rename_bevy_component
object = bpy.context.object
source_component_name = "SomeOldUnitStruct"
target_component_name = "UnitTest"
object[source_component_name] = '()'
rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name]))
is_old_component_in_object = source_component_name in object
is_new_component_in_object = target_component_name in object
assert is_old_component_in_object == False
assert is_new_component_in_object == True
assert object[target_component_name] == '()'
assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None
def test_rename_component_single_complex_struct(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
rename_component_operator = bpy.ops.object.rename_bevy_component
object = bpy.context.object
source_component_name = "ProxyCollider"
target_component_name = "Collider"
object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name]))
is_old_component_in_object = source_component_name in object
is_new_component_in_object = target_component_name in object
assert is_old_component_in_object == False
assert is_new_component_in_object == True
assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None
def test_rename_component_bulk(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
rename_component_operator = bpy.ops.object.rename_bevy_component
source_component_name = "SomeOldUnitStruct"
target_component_name = "UnitTest"
objects_names = []
for object in bpy.data.objects:
object[source_component_name] = '()'
objects_names.append(object.name)
# bulk rename
rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps(objects_names))
for object in bpy.data.objects:
is_old_component_in_object = source_component_name in object
is_new_component_in_object = target_component_name in object
assert is_old_component_in_object == False
assert is_new_component_in_object == True
assert object[target_component_name] == '()'
assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None
def test_rename_component_single_error_handling(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
rename_component_operator = bpy.ops.object.rename_bevy_component
object = bpy.context.object
source_component_name = "SomeOldUnitStruct"
target_component_name = "UnitTest"
object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
expected_error = f'Error: Failed to rename component: Errors:["wrong custom property values to generate target component: object: \'{object.name}\', error: input string too big for a unit struct"]\n'
expected_error = re.escape(expected_error)
with pytest.raises(Exception, match=expected_error):
rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name]))
target_component_metadata = get_component_metadata(object, target_component_name)
is_old_component_in_object = source_component_name in object
is_new_component_in_object = target_component_name in object
assert is_old_component_in_object == False
assert is_new_component_in_object == True
assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
assert get_component_propGroup(registry, target_component_name, target_component_metadata) != None
assert target_component_metadata.invalid == True
assert target_component_metadata.invalid_details == 'wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate'
def test_rename_component_single_error_handling_clean_errors(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
rename_component_operator = bpy.ops.object.rename_bevy_component
object = bpy.context.object
source_component_name = "SomeOldUnitStruct"
target_component_name = "UnitTest"
object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
expected_error = f'Error: Failed to rename component: Errors:["wrong custom property values to generate target component: object: \'{object.name}\', error: input string too big for a unit struct"]\n'
expected_error = re.escape(expected_error)
with pytest.raises(Exception, match=expected_error):
rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name]))
target_component_metadata = get_component_metadata(object, target_component_name)
is_old_component_in_object = source_component_name in object
is_new_component_in_object = target_component_name in object
assert is_old_component_in_object == False
assert is_new_component_in_object == True
assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)'
assert get_component_propGroup(registry, target_component_name, target_component_metadata) != None
assert target_component_metadata.invalid == True
assert target_component_metadata.invalid_details == 'wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate'
# if we fix the custom property value & regen the ui, it should be all good
regen_component_operator = bpy.ops.object.refresh_ui_from_custom_properties_current
object[target_component_name] = ''
regen_component_operator()
assert target_component_metadata.invalid == False

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "gltf_auto_export", "name": "gltf_auto_export",
"author": "kaosigh", "author": "kaosigh",
"version": (0, 15, 0), "version": (0, 16, 0),
"blender": (3, 4, 0), "blender": (3, 4, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "glTF/glb auto-export", "description": "glTF/glb auto-export",

View File

@ -4,7 +4,7 @@ import bpy
from ..helpers.generate_and_export import generate_and_export from ..helpers.generate_and_export import generate_and_export
from .export_gltf import (generate_gltf_export_preferences, export_gltf) from .export_gltf import (generate_gltf_export_preferences, export_gltf)
from ..modules.bevy_dynamic import is_object_dynamic, is_object_static from ..modules.bevy_dynamic import is_object_dynamic, is_object_static
from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into, inject_blueprints_list_into_main_scene, remove_blueprints_list_from_main_scene
# export all main scenes # export all main scenes
@ -16,6 +16,7 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs) gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_output_folder = getattr(addon_prefs,"export_output_folder") export_output_folder = getattr(addon_prefs,"export_output_folder")
export_blueprints = getattr(addon_prefs,"export_blueprints") export_blueprints = getattr(addon_prefs,"export_blueprints")
legacy_mode = getattr(addon_prefs, "export_legacy_mode")
export_separate_dynamic_and_static_objects = getattr(addon_prefs, "export_separate_dynamic_and_static_objects") export_separate_dynamic_and_static_objects = getattr(addon_prefs, "export_separate_dynamic_and_static_objects")
gltf_output_path = os.path.join(folder_path, export_output_folder, scene.name) gltf_output_path = os.path.join(folder_path, export_output_folder, scene.name)
@ -29,6 +30,9 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
} }
if export_blueprints : if export_blueprints :
if not legacy_mode:
inject_blueprints_list_into_main_scene(scene)
if export_separate_dynamic_and_static_objects: if export_separate_dynamic_and_static_objects:
#print("SPLIT STATIC AND DYNAMIC") #print("SPLIT STATIC AND DYNAMIC")
# first export static objects # first export static objects
@ -67,5 +71,8 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
print(" exporting gltf to", gltf_output_path, ".gltf/glb") print(" exporting gltf to", gltf_output_path, ".gltf/glb")
export_gltf(gltf_output_path, export_settings) export_gltf(gltf_output_path, export_settings)
if not legacy_mode:
remove_blueprints_list_from_main_scene(scene)

View File

@ -36,16 +36,23 @@ def get_marked_collections(scene, addon_prefs):
return (collection_names, marked_collections) return (collection_names, marked_collections)
# gets all collections within collections that might also be relevant # gets all collections within collections that might also be relevant
def get_sub_collections(collections, parent, children_per_collection): def get_sub_collections(collections, parent=None, children_per_collection=None):
if parent == None:
parent = CollectionNode()
if children_per_collection == None:
children_per_collection = {}
collection_names = set() collection_names = set()
used_collections = [] used_collections = []
for root_collection in collections: for root_collection in collections:
node = Node(name=root_collection.name, parent=parent) #print("collections", collections)
node = CollectionNode(name=root_collection.name, parent=parent)
parent.children.append(node) parent.children.append(node)
#print("root collection", root_collection.name) #print("root collection", root_collection.name)
for collection in traverse_tree(root_collection): # TODO: filter out COLLECTIONS that have the flatten flag (unlike the flatten flag on colleciton instances themselves) for collection in traverse_tree(root_collection): # TODO: filter out COLLECTIONS that have the flatten flag (unlike the flatten flag on colleciton instances themselves)
#print("sub", collection)
node_name = collection.name node_name = collection.name
children_per_collection[node_name] = [] children_per_collection[node_name] = []
#print(" scanning", collection.name) #print(" scanning", collection.name)
@ -53,12 +60,15 @@ def get_sub_collections(collections, parent, children_per_collection):
#print("FLATTEN", object.name, 'Flatten' in object) #print("FLATTEN", object.name, 'Flatten' in object)
if object.instance_type == 'COLLECTION' : # and not 'Flatten' in object: if object.instance_type == 'COLLECTION' : # and not 'Flatten' in object:
collection_name = object.instance_collection.name collection_name = object.instance_collection.name
#print("sub obj", collection_name)
# FIXME: not sure:
children_per_collection[node_name].append(collection_name)
(sub_names, sub_collections) = get_sub_collections([object.instance_collection], node, children_per_collection) (sub_names, sub_collections) = get_sub_collections([object.instance_collection], node, children_per_collection)
if len(list(sub_names)) > 0: if len(list(sub_names)) > 0:
children_per_collection[node_name] += (list(sub_names)) children_per_collection[node_name] += (list(sub_names))
#print(" found sub collection in use", object.name, object.instance_collection) #print(" found sub collection in use", object.name, object.instance_collection)
if not collection_name in collection_names: if not collection_name in collection_names:
collection_names.add(collection_name) collection_names.add(collection_name)
used_collections.append(object.instance_collection) used_collections.append(object.instance_collection)
@ -77,7 +87,7 @@ def flatten_collection_tree(node, children_per_collection):
children_per_collection[node.name] = list(set( children_per_collection[node.name])) children_per_collection[node.name] = list(set( children_per_collection[node.name]))
class Node : class CollectionNode :
def __init__(self, name="", parent=None): def __init__(self, name="", parent=None):
self.name = name self.name = name
self.children = [] self.children = []
@ -93,7 +103,7 @@ def get_exportable_collections(main_scenes, library_scenes, addon_prefs):
all_collections = [] all_collections = []
all_collection_names = [] all_collection_names = []
root_node = Node() root_node = CollectionNode()
root_node.name = "root" root_node.name = "root"
children_per_collection = {} children_per_collection = {}

View File

@ -1,5 +1,6 @@
import json
import bpy import bpy
from .helpers_collections import (set_active_collection) from .helpers_collections import (CollectionNode, get_sub_collections, get_used_collections, set_active_collection)
from .object_makers import (make_empty) from .object_makers import (make_empty)
@ -73,6 +74,17 @@ def copy_hollowed_collection_into(source_collection, destination_collection, par
empty_obj['BlueprintName'] = '"'+collection_name+'"' if legacy_mode else '("'+collection_name+'")' empty_obj['BlueprintName'] = '"'+collection_name+'"' if legacy_mode else '("'+collection_name+'")'
empty_obj['SpawnHere'] = '()' empty_obj['SpawnHere'] = '()'
# we also inject a list of all sub blueprints, so that the bevy side can preload them
if not legacy_mode:
root_node = CollectionNode()
root_node.name = "root"
children_per_collection = {}
print("collection stuff", original_name)
get_sub_collections([object.instance_collection], root_node, children_per_collection)
empty_obj["BlueprintsList"] = f"({json.dumps(dict(children_per_collection))})"
#empty_obj["Assets"] = {"Animations": [], "Materials": [], "Models":[], "Textures":[], "Audio":[], "Other":[]}
# we copy custom properties over from our original object to our empty # we copy custom properties over from our original object to our empty
for component_name, component_value in object.items(): for component_name, component_value in object.items():
if component_name not in custom_properties_to_filter_out and is_component_valid(object, component_name): #copy only valid properties if component_name not in custom_properties_to_filter_out and is_component_valid(object, component_name): #copy only valid properties
@ -149,3 +161,45 @@ def get_scenes(addon_prefs):
return [level_scene_names, level_scenes, library_scene_names, library_scenes] return [level_scene_names, level_scenes, library_scene_names, library_scenes]
def inject_blueprints_list_into_main_scene(scene):
print("injecting assets/blueprints data into scene")
root_collection = scene.collection
assets_list = None
assets_list_name = f"assets_list_{scene.name}_components"
for object in scene.objects:
if object.name == assets_list_name:
assets_list = object
break
if assets_list is None:
assets_list = make_empty(assets_list_name, [0,0,0], [0,0,0], [0,0,0], root_collection)
# find all blueprints used in a scene
# TODO: export a tree rather than a flat list ? because you could have potential clashing items in flat lists (amongst other issues)
(collection_names, collections) = get_used_collections(scene)
root_node = CollectionNode()
root_node.name = "root"
children_per_collection = {}
#print("collection_names", collection_names, "collections", collections)
get_sub_collections(collections, root_node, children_per_collection)
# what about marked assets ?
# what about audio assets ?
# what about materials ?
# object['MaterialInfo'] = '(name: "'+material.name+'", source: "'+current_project_name + '")'
#assets_list["blueprints_direct"] = list(collection_names)
assets_list["BlueprintsList"] = f"({json.dumps(dict(children_per_collection))})"
#assets_list["Materials"]= '()'
def remove_blueprints_list_from_main_scene(scene):
assets_list = None
assets_list_name = f"assets_list_{scene.name}_components"
for object in scene.objects:
if object.name == assets_list_name:
assets_list = object
if assets_list is not None:
bpy.data.objects.remove(assets_list, do_unlink=True)

View File

@ -19,6 +19,8 @@ def setup_data(request):
def finalizer(): def finalizer():
print("\nPerforming teardown...") print("\nPerforming teardown...")
get_orphan_data()
if os.path.exists(models_path): if os.path.exists(models_path):
shutil.rmtree(models_path) shutil.rmtree(models_path)
@ -33,6 +35,11 @@ def setup_data(request):
return None return None
def get_orphan_data():
orphan_meshes = [m.name for m in bpy.data.meshes if m.users == 0]
# print("orphan meshes before", orphan_meshes)
def test_export_do_not_export_blueprints(setup_data): def test_export_do_not_export_blueprints(setup_data):
auto_export_operator = bpy.ops.export_scenes.auto_gltf auto_export_operator = bpy.ops.export_scenes.auto_gltf
@ -57,7 +64,6 @@ def test_export_do_not_export_blueprints(setup_data):
def test_export_custom_blueprints_path(setup_data): def test_export_custom_blueprints_path(setup_data):
auto_export_operator = bpy.ops.export_scenes.auto_gltf auto_export_operator = bpy.ops.export_scenes.auto_gltf
# first, configure things # first, configure things
# we use the global settings for that # we use the global settings for that
export_props = { export_props = {
@ -210,3 +216,27 @@ def test_export_separate_dynamic_and_static_objects(setup_data):
assert os.path.exists(os.path.join(setup_data["models_path"], "World.glb")) == True assert os.path.exists(os.path.join(setup_data["models_path"], "World.glb")) == True
assert os.path.exists(os.path.join(setup_data["models_path"], "World_dynamic.glb")) == True assert os.path.exists(os.path.join(setup_data["models_path"], "World_dynamic.glb")) == True
def test_export_should_not_generate_orphan_data(setup_data):
auto_export_operator = bpy.ops.export_scenes.auto_gltf
# first, configure things
# we use the global settings for that
export_props = {
"main_scene_names" : ['World'],
"library_scene_names": ['Library']
}
stored_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings")
stored_settings.clear()
stored_settings.write(json.dumps(export_props))
auto_export_operator(
direct_mode=True,
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=False,
)
assert os.path.exists(os.path.join(setup_data["models_path"], "World.glb")) == True
assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint1.glb")) == False

View File

@ -16,18 +16,16 @@ def setup_data(request):
root_path = "../../testing/bevy_example" root_path = "../../testing/bevy_example"
assets_root_path = os.path.join(root_path, "assets") assets_root_path = os.path.join(root_path, "assets")
models_path = os.path.join(assets_root_path, "models") models_path = os.path.join(assets_root_path, "models")
#materials_path = os.path.join("../../testing", "materials") materials_path = os.path.join(assets_root_path, "materials")
#other_materials_path = os.path.join("../../testing", "other_materials") #other_materials_path = os.path.join("../../testing", "other_materials")
print("\nPerforming teardown...") print("\nPerforming teardown...")
if os.path.exists(models_path): if os.path.exists(models_path):
shutil.rmtree(models_path) shutil.rmtree(models_path)
"""if os.path.exists(materials_path): if os.path.exists(materials_path):
shutil.rmtree(materials_path) shutil.rmtree(materials_path)
if os.path.exists(other_materials_path):
shutil.rmtree(other_materials_path)"""
diagnostics_file_path = os.path.join(root_path, "bevy_diagnostics.json") diagnostics_file_path = os.path.join(root_path, "bevy_diagnostics.json")
if os.path.exists(diagnostics_file_path): if os.path.exists(diagnostics_file_path):
os.remove(diagnostics_file_path) os.remove(diagnostics_file_path)
@ -75,7 +73,8 @@ def test_export_complex(setup_data):
export_scene_settings=True, export_scene_settings=True,
export_blueprints=True, export_blueprints=True,
export_legacy_mode=False, export_legacy_mode=False,
export_animations=True export_animations=True,
export_materials_library=True
) )
# blueprint1 => has an instance, got changed, should export # blueprint1 => has an instance, got changed, should export
# blueprint2 => has NO instance, but marked as asset, should export # blueprint2 => has NO instance, but marked as asset, should export
@ -84,7 +83,6 @@ def test_export_complex(setup_data):
# blueprint5 => has NO instance, not marked as asset, should NOT export # blueprint5 => has NO instance, not marked as asset, should NOT export
assert os.path.exists(os.path.join(models_path, "World.glb")) == True assert os.path.exists(os.path.join(models_path, "World.glb")) == True
assert os.path.exists(os.path.join(models_path, "library", "Blueprint1.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint1.glb")) == True
assert os.path.exists(os.path.join(models_path, "library", "Blueprint2.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint2.glb")) == True
assert os.path.exists(os.path.join(models_path, "library", "Blueprint3.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint3.glb")) == True
@ -93,13 +91,14 @@ def test_export_complex(setup_data):
assert os.path.exists(os.path.join(models_path, "library", "Blueprint6_animated.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint6_animated.glb")) == True
assert os.path.exists(os.path.join(models_path, "library", "Blueprint7_hierarchy.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint7_hierarchy.glb")) == True
# 'assets_list_'+scene.name+"_components" should have been removed after the export
assets_list_object_name = "assets_list_"+"World"+"_components"
assets_list_object_present = assets_list_object_name in bpy.data.objects
assert assets_list_object_present == False
# now run bevy # now run bevy
command = "cargo run --features bevy/dynamic_linking" command = "cargo run --features bevy/dynamic_linking"
# assert getattr(propertyGroup, 'a') == 0.5714026093482971
FNULL = open(os.devnull, 'w') #use this if you want to suppress output to stdout from the subprocess FNULL = open(os.devnull, 'w') #use this if you want to suppress output to stdout from the subprocess
filename = "my_file.dat"
args = command
#subprocess.call(args, stdout=FNULL, stderr=FNULL, shell=False, cwd=bevy_run_exec_path)
return_code = subprocess.call(["cargo", "run", "--features", "bevy/dynamic_linking"], cwd=root_path) return_code = subprocess.call(["cargo", "run", "--features", "bevy/dynamic_linking"], cwd=root_path)
print("RETURN CODE OF BEVY APP", return_code) print("RETURN CODE OF BEVY APP", return_code)
assert return_code == 0 assert return_code == 0
@ -110,6 +109,7 @@ def test_export_complex(setup_data):
assert diagnostics["animations"] == True assert diagnostics["animations"] == True
assert diagnostics["cylinder_found"] == True assert diagnostics["cylinder_found"] == True
assert diagnostics["empty_found"] == True assert diagnostics["empty_found"] == True
assert diagnostics["blueprints_list_found"] == True
# last but not least, do a visual compare # last but not least, do a visual compare
screenshot_expected_path = os.path.join(root_path, "expected_screenshot.png") screenshot_expected_path = os.path.join(root_path, "expected_screenshot.png")