feat(tools/bevy_blueprints): bevy plugin + blender addon for components UI to more easily create components (#70)

* adds a new crate: ```bevy_registry_export``` to be able to create a json import of the registered component/type definitions
* adds a new Blender addon: ```bevy_components``` that takes that json data to generate custom UIs for components , to be to add & edit components easily in Blender 
   * also adds component metadata per object for more advanced features
   * etc
* updates to bevy_gltf_components & bevy_gltf_blueprints to add legacy_mode to support the "old"/current style component definitions
* same with gltf_auto_export Blender add_on
* closes #60
This commit is contained in:
Mark Moissette 2024-02-05 23:01:19 +01:00 committed by GitHub
parent 5c8f41b4a6
commit cfbda24da7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 19916 additions and 178 deletions

View File

@ -3,6 +3,7 @@ members = [
"crates/bevy_gltf_components", "crates/bevy_gltf_components",
"crates/bevy_gltf_blueprints", "crates/bevy_gltf_blueprints",
"crates/bevy_gltf_save_load", "crates/bevy_gltf_save_load",
"crates/bevy_registry_export",
"examples/bevy_gltf_components/basic/", "examples/bevy_gltf_components/basic/",
"examples/bevy_gltf_components/basic_wasm/", "examples/bevy_gltf_components/basic_wasm/",
"examples/bevy_gltf_blueprints/basic/", "examples/bevy_gltf_blueprints/basic/",
@ -15,6 +16,7 @@ members = [
"examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles", "examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles",
"examples/bevy_gltf_blueprints/materials/", "examples/bevy_gltf_blueprints/materials/",
"examples/bevy_gltf_save_load/basic/", "examples/bevy_gltf_save_load/basic/",
"examples/bevy_registry_export/basic"
] ]
resolver = "2" resolver = "2"

View File

@ -0,0 +1,73 @@
# Workflow: classic
The workflow goes as follows (once you got your Bevy code setup)
- create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed)
![component registration](./docs/component_registration.png)
## Component creation
- Create an object / collection (for reuse) in Blender
- Go to object properties => add a property, and add your component data
- unit structs, enums, and more complex strucs / components are all supported, (if the fields are basic data types at least,
have not tried more complex ones yet, but should also work)
- for structs with no params (unit structs): use a **STRING** property & an empty value
- for structs with params: use a RON representation of your fields (see below)
- for tupple strucs you can use any of the built in Blender custom property types: Strings, Booleans, floats, Vectors, etc
![unit struct components in Blender](./docs/components_blender.png)
In rust:
![unit struct components in Bevy](./docs/demo_simple_components.png)
(the Rust struct for these components for reference is [here](./examples/basic/game.rs#34) )
![complex components in Blender](./docs/components_blender_parameters.png)
In rust:
![complex components in Blender](./docs/camera_tracking_component.png)
(the Rust struct for this component for reference is [here](./examples/basic/core/camera/camera_tracking.rs#21) )
There is an other examples of using various Component types: Enums, Tupple structs, strucs with fields etc [here](./examples/basic/test_components.rs),
even colors, Vecs (arrays), Vec2, Vec3 etc are all supported
![complex components in Blender](./docs/components_blender_parameters2.png)
- for collections & their instances:
* I usually create a library scene with nested collections
* the leaf collections are the assets you use in your level
* add an empty called xxxx_components
* add the components as explained in the previous part
![blender collection asset](./docs/blender_collections.png)
* In the Level/world itself, just create an instance of the collection (standard Blender, ie Shift+A -> collection instance -> pick the collection)
## Exporting to gltf
- export your level as a glb/gltf file :
- using Blender's default gltf exporter
!!**IMPORTANT** you need to check the following:
- custom properties
- cameras & lights if you want a complete level (as in this example)
![gltf_export](./docs/gltf_export.png)
- or much better, using [gltf_auto_export](./tools/gltf_auto_export/)
## Now use your gltf files in Bevy
- load it in Bevy (see the various examples for this)
- you should see the components attached to your entities in Bevy
![components in bevy](./docs/components_bevy.png)
![components in bevy](./docs/components_bevy2.png)
![components in bevy](./docs/components_bevy3.png)
> note: you get a warning if there are any unregistered components in your gltf file (they get ignored)
you will get a warning **per entity**
![missing components warnings](./docs/component_warnings.png)

40
README-workflow-ui.md Normal file
View File

@ -0,0 +1,40 @@
# Workflow: UI
The workflow goes as follows (once you got your Bevy code setup)
## Bevy side
- create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed)
- follow the instructions in the [bevy_registry_export](./crates/bevy_registry_export/) to generate a registry export
## Component creation
Setup the Blender [bevy_components](./tools/bevy_components/README.md) add-on
to add & edit your components visually & reliably
![bevy_components](./docs/bevy_components.png)
## Exporting to gltf
- export your level as a glb/gltf file :
- using Blender's default gltf exporter
!!**IMPORTANT** you need to check the following:
- custom properties
- cameras & lights if you want a complete level (as in this example)
![gltf_export](./docs/gltf_export.png)
- or much better, using [gltf_auto_export](./tools/gltf_auto_export/)
## Now use your gltf files in Bevy
- load it in Bevy (see the various examples for this)
- you should see the components attached to your entities in Bevy
![components in bevy](./docs/components_bevy.png)
![components in bevy](./docs/components_bevy2.png)
![components in bevy](./docs/components_bevy3.png)
> note: you get a warning if there are any unregistered components in your gltf file (they get ignored)
you will get a warning **per entity**
![missing components warnings](./docs/component_warnings.png)

104
README.md
View File

@ -18,7 +18,8 @@ It also allows you to setup 'blueprints' in Blender by using collections (the re
* Useful if you want to use Blender (or any editor allowing to export gltf with configurable gltf_extras) as your Editor * Useful if you want to use Blender (or any editor allowing to export gltf with configurable gltf_extras) as your Editor
* define Bevy components as custom properties in Blender (some visually , some using RON, though an older JSON version is also available) * define Bevy components as custom properties in Blender (some visually , some using RON, though an older JSON version is also available)
* no plugin or extra tools needed in Blender (but I provide a [little Blender plugin](./tools/gltf_auto_export/README.md) to auto-export to gltf on save (and more !) if you want !) * no plugin or extra tools needed in Blender (but I provide a [little Blender add-on](./tools/gltf_auto_export/README.md) to auto-export to gltf on save (and more !) if you want !)
* now also with a nice UI tool to add & edit Bevy components in [Blender](./tools/bevy_components/README.md)
* define components in Blender Collections & override any of them in your collection instances if you want * define components in Blender Collections & override any of them in your collection instances if you want
* ability to automatically turn your Blender collections into [gltf Blueprints](./crates/bevy_gltf_blueprints/README.md) for reuse * ability to automatically turn your Blender collections into [gltf Blueprints](./crates/bevy_gltf_blueprints/README.md) for reuse
* minimal setup & code, you can have something basic running fast * minimal setup & code, you can have something basic running fast
@ -29,18 +30,23 @@ It also allows you to setup 'blueprints' in Blender by using collections (the re
## Crates ## Crates
- [bevy_gltf_components](./crates/bevy_gltf_components/) This crate allows you to define components direclty inside gltf files and instanciate/inject the components on the Bevy side. - [bevy_gltf_components](./crates/bevy_gltf_components/) This crate allows you to define components direclty inside gltf files and instanciate/inject the components on the Bevy side.
There is a [video tutorial/explanation](https://youtu.be/-lcScjQCA3c) if you want, or you can read the crate docs.
The examples for the crate are [here](./examples/bevy_gltf_components/) There is a [video tutorial/explanation](https://youtu.be/-lcScjQCA3c) if you want, or you can read the crate docs.
The examples for the crate are [here](./examples/bevy_gltf_components/)
- [bevy_gltf_blueprints](./crates/bevy_gltf_blueprints/) This crate adds the ability to define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy. With the ability to override and add components when spawning, efficient "level" loading etc - [bevy_gltf_blueprints](./crates/bevy_gltf_blueprints/) This crate adds the ability to define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy. With the ability to override and add components when spawning, efficient "level" loading etc
There is a [video tutorial/explanation](https://youtu.be/CgyNtwgYwdM) for this one too, or you can read the crate docs
The examples for the crate are [here](./examples/bevy_gltf_blueprints/) There is a [video tutorial/explanation](https://youtu.be/CgyNtwgYwdM) for this one too, or you can read the crate docs
> Note: this is the recomended crate to use and uses ```bevy_gltf_components``` under the hood The examples for the crate are [here](./examples/bevy_gltf_blueprints/)
> Note: this is the recomended crate to use and uses ```bevy_gltf_components``` under the hood
- [bevy_gltf_save_load](./crates/bevy_gltf_save_load/) This crate adds the ability to save & load your game state in a relatively simple way, by leveraging the blueprint functionality of - [bevy_gltf_save_load](./crates/bevy_gltf_save_load/) This crate adds the ability to save & load your game state in a relatively simple way, by leveraging the blueprint functionality of
bevy_gltf_blueprints to only save a minimal subset of dynamic data, seperating dynamic & static parts of levels etc. bevy_gltf_blueprints to only save a minimal subset of dynamic data, seperating dynamic & static parts of levels etc.
The examples for the crate are [here](./examples/bevy_gltf_save_load/) The examples for the crate are [here](./examples/bevy_gltf_save_load/)
> Note: this uses ```bevy_gltf_blueprints``` under the hood > Note: this uses ```bevy_gltf_blueprints``` under the hood
- [bevy_registry_export](./crates/bevy_registry_export/) This crate adds the ability to export your project's Bevy registry to json, in order to be able to generate custom component UIs on the Blender side in the Blender [bevy_components](./tools/bevy_components/README.md) add-on
## Tools ## Tools
@ -51,6 +57,13 @@ The examples for the crate are [here](./examples/bevy_gltf_save_load/)
Please read the [README]((./tools/gltf_auto_export/README.md)) of the add-on for installation & use instructions Please read the [README]((./tools/gltf_auto_export/README.md)) of the add-on for installation & use instructions
### Blender: bevy_components
- an add-on for Blender to allow easilly adding & editing Bevy components , using automatically generated UIs for each component
Please read the [README]((./tools/bevy_components/README.md)) of the add-on for installation & use instructions
## Examples ## Examples
@ -62,6 +75,12 @@ you can find all examples, by crate as seperate crates for clearer dependencies
- [bevy_gltf_blueprints](./examples/bevy_gltf_blueprints/) - [bevy_gltf_blueprints](./examples/bevy_gltf_blueprints/)
* [basic](./examples/bevy_gltf_blueprints/basic/) more advanced example : use of ```bevy_gltf_blueprints``` to spawn a level and then populate it with entities coming from different gltf files, live (at runtime) spawning of entities etc * [basic](./examples/bevy_gltf_blueprints/basic/) more advanced example : use of ```bevy_gltf_blueprints``` to spawn a level and then populate it with entities coming from different gltf files, live (at runtime) spawning of entities etc
* [animation](./examples/bevy_gltf_blueprints/animation/) how to use and trigger animations from gltf files (a feature of ```bevy_gltf_blueprints```) * [animation](./examples/bevy_gltf_blueprints/animation/) how to use and trigger animations from gltf files (a feature of ```bevy_gltf_blueprints```)
* & lots more
- [bevy_gltf_save_load](./examples/bevy_gltf_save_load/)
- [bevy_registry_export](./examples/bevy_registry_export/)
## Workflow ## Workflow
@ -70,74 +89,12 @@ The workflow goes as follows (once you got your Bevy code setup)
- create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed) - create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed)
![component registration](./docs/component_registration.png) ![component registration](./docs/component_registration.png)
- Create an object / collection (for reuse) in Blender
- Go to object properties => add a property, and add your component data
- unit structs, enums, and more complex strucs / components are all supported, (if the fields are basic data types at least,
have not tried more complex ones yet, but should also work)
- for structs with no params (unit structs): use a **STRING** property & an empty value
- for structs with params: use a RON representation of your fields (see below)
- for tupple strucs you can use any of the built in Blender custom property types: Strings, Booleans, floats, Vectors, etc
![unit struct components in Blender](./docs/components_blender.png)
In rust:
![unit struct components in Bevy](./docs/demo_simple_components.png)
(the Rust struct for these components for reference is [here](./examples/basic/game.rs#34) )
![complex components in Blender](./docs/components_blender_parameters.png)
In rust:
![complex components in Blender](./docs/camera_tracking_component.png)
(the Rust struct for this component for reference is [here](./examples/basic/core/camera/camera_tracking.rs#21) )
There is an other examples of using various Component types: Enums, Tupple structs, strucs with fields etc [here](./examples/basic/test_components.rs),
even colors, Vecs (arrays), Vec2, Vec3 etc are all supported
![complex components in Blender](./docs/components_blender_parameters2.png)
- for collections & their instances:
* I usually create a library scene with nested collections
* the leaf collections are the assets you use in your level
* add an empty called xxxx_components
* add the components as explained in the previous part
![blender collection asset](./docs/blender_collections.png)
* In the Level/world itself, just create an instance of the collection (standard Blender, ie Shift+A -> collection instance -> pick the collection)
- export your level as a glb/gltf file :
- using Blender's default gltf exporter
!!**IMPORTANT** you need to check the following:
- custom properties
- cameras & lights if you want a complete level (as in this example)
![gltf_export](./docs/gltf_export.png)
- or much better, using [gltf_auto_export](./tools/gltf_auto_export/)
- load it in Bevy (see the demo main file for this)
- you should see the components attached to your entities in Bevy
![components in bevy](./docs/components_bevy.png)
![components in bevy](./docs/components_bevy2.png)
![components in bevy](./docs/components_bevy3.png)
> note: you get a warning if there are any unregistered components in your gltf file (they get ignored)
you will get a warning **per entity**
![missing components warnings](./docs/component_warnings.png)
- then you have two options
- add your components to objects in Blender **manually** as **custom properties** : see [here](./README-workflow-classic.md) for more details
- add your components to objects in Blender **with a nice UI** see [here](./README-workflow-ui.md) for more details
## Limitations / issues ## Limitations / issues
- some components have to be defined in ```text``` in Blender, might try using the AppTypeRegistry and some Python code on the Blender side for a nicer UI (although this loses the "fast & easy, no tooling" approach)
- Some of `bevy_rapier`/physics code / ways to define colliders could perhaps be done better/visually within Blender (currently it also goes via RON) - Some of `bevy_rapier`/physics code / ways to define colliders could perhaps be done better/visually within Blender (currently it also goes via RON)
## Future work ## Future work
@ -153,6 +110,9 @@ Thanks to all the contributors helping out with this project ! Big kudos to you,
- [GitGhillie](https://github.com/GitGhillie) - [GitGhillie](https://github.com/GitGhillie)
- [Azorlogh](https://github.com/Azorlogh) - [Azorlogh](https://github.com/Azorlogh)
- [BSDGuyShawn](https://github.com/BSDGuyShawn)
- [yukkop](https://github.com/yukkop)
- [killercup](https://github.com/killercup)
## License ## License

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bevy_gltf_blueprints" name = "bevy_gltf_blueprints"
version = "0.6.0" version = "0.7.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](https://bevyengine.org/) 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"
@ -17,5 +17,6 @@ workspace = true
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] } bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] }
[dependencies] [dependencies]
bevy_gltf_components = "0.2" bevy_gltf_components = "0.3"
#bevy_gltf_components = { path = "../bevy_gltf_components" }
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] } bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] }

View File

@ -26,7 +26,7 @@ Here's a minimal usage example:
# Cargo.toml # Cargo.toml
[dependencies] [dependencies]
bevy="0.12" bevy="0.12"
bevy_gltf_blueprints = { version = "0.6"} bevy_gltf_blueprints = { version = "0.7"}
``` ```
@ -64,7 +64,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.6" bevy_gltf_blueprints = "0.7"
``` ```
Or use `cargo add`: Or use `cargo add`:
@ -307,6 +307,25 @@ see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/exa
Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export) Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
## Legacy mode
Starting in version 0.7 there is a new parameter ```legacy_mode``` for backwards compatibility
To disable the legacy mode: (enabled by default)
```rust no_run
BlueprintsPlugin{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) !
As it create custom properties that are writen in real **ron** file format instead of a simplified version (the one in the legacy mode)
> Note: the legacy mode support will be dropped in future versions, and the default behaviour will be NO legacy mode
## Examples ## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic
@ -333,7 +352,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.3 - 0.6` | `0.12` | | `0.3 - 0.7` | `0.12` |
| `0.1 - 0.2` | `0.11` | | `0.1 - 0.2` | `0.11` |
| branch `main` | `0.12` | | branch `main` | `0.12` |
| branch `bevy_main` | `main` | | branch `bevy_main` | `main` |

View File

@ -78,6 +78,8 @@ impl fmt::Display for GltfFormat {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Plugin for gltf blueprints /// Plugin for gltf blueprints
pub struct BlueprintsPlugin { pub struct BlueprintsPlugin {
pub legacy_mode: bool, // flag that gets passed on to bevy_gltf_components
pub format: GltfFormat, pub format: GltfFormat,
/// The base folder where library/blueprints assets are loaded from, relative to the executable. /// The base folder where library/blueprints assets are loaded from, relative to the executable.
pub library_folder: PathBuf, pub library_folder: PathBuf,
@ -91,6 +93,7 @@ pub struct BlueprintsPlugin {
impl Default for BlueprintsPlugin { impl Default for BlueprintsPlugin {
fn default() -> Self { fn default() -> Self {
Self { Self {
legacy_mode: true,
format: GltfFormat::GLB, format: GltfFormat::GLB,
library_folder: PathBuf::from("models/library"), library_folder: PathBuf::from("models/library"),
aabbs: false, aabbs: false,
@ -110,7 +113,9 @@ fn materials_library_enabled(blueprints_config: Res<BluePrintsConfig>) -> bool {
impl Plugin for BlueprintsPlugin { impl Plugin for BlueprintsPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(ComponentsFromGltfPlugin) app.add_plugins(ComponentsFromGltfPlugin{
legacy_mode: self.legacy_mode
})
.register_type::<BlueprintName>() .register_type::<BlueprintName>()
.register_type::<MaterialInfo>() .register_type::<MaterialInfo>()
.register_type::<SpawnHere>() .register_type::<SpawnHere>()

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bevy_gltf_components" name = "bevy_gltf_components"
version = "0.2.2" version = "0.3.0"
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](https://bevyengine.org/) 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"

View File

@ -24,7 +24,7 @@ Here's a minimal usage example:
# Cargo.toml # Cargo.toml
[dependencies] [dependencies]
bevy="0.12" bevy="0.12"
bevy_gltf_components = { version = "0.2"} bevy_gltf_components = { version = "0.3"}
``` ```
@ -33,7 +33,7 @@ bevy_gltf_components = { version = "0.2"}
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugin(ComponentsFromGltfPlugin) .add_plugin(ComponentsFromGltfPlugin::default())
.add_system(spawn_level) .add_system(spawn_level)
.run(); .run();
} }
@ -60,7 +60,7 @@ bevy_gltf_components = { version = "0.2"}
Add the following to your `[dependencies]` section in `Cargo.toml`: Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml ```toml
bevy_gltf_components = "0.2" bevy_gltf_components = "0.3"
``` ```
Or use `cargo add`: Or use `cargo add`:
@ -69,6 +69,27 @@ Or use `cargo add`:
cargo add bevy_gltf_components cargo add bevy_gltf_components
``` ```
## Configuration
starting with version 0.3, this plugin is configurable
Use the default configuration:
```rust no_run
ComponentsFromGltfPlugin::default()
```
Or disable the legacy mode: (enabled by default)
```rust no_run
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) !
As it create custom properties that are writen in real **ron** file format
instead of a simplified version (the one in the legacy mode)
> Note: the legacy mode support will be dropped in future versions, and the default behaviour will be NO legacy mode
## SystemSet ## SystemSet
the ordering of systems is very important ! the ordering of systems is very important !
@ -96,7 +117,7 @@ The main branch is compatible with the latest Bevy release, while the branch `be
Compatibility of `bevy_gltf_components` versions: Compatibility of `bevy_gltf_components` versions:
| `bevy_gltf_components` | `bevy` | | `bevy_gltf_components` | `bevy` |
| :-- | :-- | | :-- | :-- |
| `0.2` | `0.12` | | `0.2 - 0.3` | `0.12` |
| `0.1` | `0.11` | | `0.1` | `0.11` |
| branch `main` | `0.12` | | branch `main` | `0.12` |
| branch `bevy_main` | `main` | | branch `bevy_main` | `main` |

View File

@ -1,11 +1,12 @@
use bevy::ecs::component::Component;
use bevy::render::color::Color;
use core::ops::Deref; use core::ops::Deref;
use ron::Value; use ron::Value;
use serde::de::DeserializeSeed; use serde::de::DeserializeSeed;
use bevy::ecs::{entity::Entity, reflect::ReflectComponent}; use bevy::ecs::{entity::Entity, reflect::ReflectComponent};
use bevy::gltf::{Gltf, GltfExtras}; use bevy::gltf::{Gltf, GltfExtras};
use bevy::reflect::serde::UntypedReflectDeserializer; // ReflectSerializer use bevy::reflect::serde::UntypedReflectDeserializer;
use bevy::reflect::{Reflect, TypeInfo, TypeRegistry}; use bevy::reflect::{Reflect, TypeInfo, TypeRegistry};
use bevy::scene::Scene; use bevy::scene::Scene;
use bevy::utils::HashMap; use bevy::utils::HashMap;
@ -16,9 +17,18 @@ use bevy::{
use super::capitalize_first_letter; use super::capitalize_first_letter;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTestColor(Color);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct VecOfColors(Vec<Color>);
pub fn ronstring_to_reflect_component( pub fn ronstring_to_reflect_component(
ron_string: &String, ron_string: &String,
type_registry: &TypeRegistry, type_registry: &TypeRegistry,
simplified_types: bool,
) -> Vec<Box<dyn Reflect>> { ) -> Vec<Box<dyn Reflect>> {
let lookup: HashMap<String, Value> = ron::from_str(ron_string.as_str()).unwrap(); let lookup: HashMap<String, Value> = ron::from_str(ron_string.as_str()).unwrap();
let mut components: Vec<Box<dyn Reflect>> = Vec::new(); let mut components: Vec<Box<dyn Reflect>> = Vec::new();
@ -38,6 +48,7 @@ pub fn ronstring_to_reflect_component(
type_registry.get_with_short_type_path(capitalized_type_name.as_str()) type_registry.get_with_short_type_path(capitalized_type_name.as_str())
{ {
debug!("TYPE INFO {:?}", type_registration.type_info()); debug!("TYPE INFO {:?}", type_registration.type_info());
if simplified_types {
match type_registration.type_info() { match type_registration.type_info() {
TypeInfo::TupleStruct(info) => { TypeInfo::TupleStruct(info) => {
// we handle tupple strucs with only one field differently, as Blender's custom properties with custom ui (float, int, bool, etc) always give us a tupple struct // we handle tupple strucs with only one field differently, as Blender's custom properties with custom ui (float, int, bool, etc) always give us a tupple struct
@ -46,20 +57,6 @@ pub fn ronstring_to_reflect_component(
.field_at(0) .field_at(0)
.expect("we should always have at least one field here"); .expect("we should always have at least one field here");
let field_name = field.type_path(); let field_name = field.type_path();
// TODO: find a way to cast with typeId instead of this matching
/*match field.type_id(){
TypeId::of::<f32>() => {
println!("WE HAVE A f32");
}
}
Vec3 => {
println!("WE HAVE A VEC3");
let bla:Vec3 = ron::from_str(&parsed_value).unwrap();
println!("bla {}", bla)
}
_ =>{}
}*/
let mut formated = parsed_value.clone(); let mut formated = parsed_value.clone();
match field_name { match field_name {
"f32" => { "f32" => {
@ -89,8 +86,10 @@ pub fn ronstring_to_reflect_component(
} }
"glam::Vec3" => { "glam::Vec3" => {
let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap(); let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap();
formated = formated = format!(
format!("(x:{},y:{},z:{})", parsed[0], parsed[1], parsed[2]); "(x:{},y:{},z:{})",
parsed[0], parsed[1], parsed[2]
);
} }
"bevy_render::color::Color" => { "bevy_render::color::Color" => {
let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap(); let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap();
@ -115,6 +114,7 @@ pub fn ronstring_to_reflect_component(
} }
_ => {} _ => {}
} }
}
// println!("parsed value {}",parsed_value); // println!("parsed value {}",parsed_value);
if parsed_value.is_empty() { if parsed_value.is_empty() {
@ -128,7 +128,7 @@ pub fn ronstring_to_reflect_component(
); );
// usefull to determine what an entity looks like Serialized // usefull to determine what an entity looks like Serialized
/*let test_struct = Enemy::default(); /*let test_struct = VecOfColors(vec![Color::Rgba { red: 0., green: 0.0, blue: 0.0, alpha: 0.0 }]);//TuppleTestColor(Color::Rgba { red: 0., green: 0.0, blue: 0.0, alpha: 0.0 });
let serializer = ReflectSerializer::new(&test_struct, &type_registry); let serializer = ReflectSerializer::new(&test_struct, &type_registry);
let serialized = let serialized =
ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap(); ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap();
@ -162,8 +162,13 @@ pub fn gltf_extras_to_components(
gltf: &mut Gltf, gltf: &mut Gltf,
scenes: &mut ResMut<Assets<Scene>>, scenes: &mut ResMut<Assets<Scene>>,
type_registry: impl Deref<Target = TypeRegistry>, type_registry: impl Deref<Target = TypeRegistry>,
legacy_mode: bool
) { ) {
let mut added_components = 0; let mut added_components = 0;
let simplified_types = legacy_mode;
if simplified_types {
warn!("using simplified component definitions is deprecated since 0.3, prefer defining components with real ron values (use the bevy_components tool for Blender for simplicity) ");
}
for (_name, scene) in &gltf.named_scenes { for (_name, scene) in &gltf.named_scenes {
debug!("gltf: scene name {:?}", _name); debug!("gltf: scene name {:?}", _name);
@ -173,7 +178,8 @@ pub fn gltf_extras_to_components(
let mut entity_components: HashMap<Entity, Vec<Box<dyn Reflect>>> = HashMap::new(); let mut entity_components: HashMap<Entity, Vec<Box<dyn Reflect>>> = HashMap::new();
for (entity, name, extras, parent) in query.iter(&scene.world) { for (entity, name, extras, parent) in query.iter(&scene.world) {
debug!("Name: {}, entity {:?}, parent: {:?}", name, entity, parent); debug!("Name: {}, entity {:?}, parent: {:?}", name, entity, parent);
let reflect_components = ronstring_to_reflect_component(&extras.value, &type_registry); let reflect_components =
ronstring_to_reflect_component(&extras.value, &type_registry, simplified_types);
added_components = reflect_components.len(); added_components = reflect_components.len();
debug!("Found components {}", added_components); debug!("Found components {}", added_components);

View File

@ -7,7 +7,7 @@ pub use gltf_to_components::*;
pub mod process_gltfs; pub mod process_gltfs;
pub use process_gltfs::*; pub use process_gltfs::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update}; use bevy::{ecs::system::Resource, prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update}};
/// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities /// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities
/// It will automatically run every time you load a gltf file /// It will automatically run every time you load a gltf file
@ -34,7 +34,7 @@ use bevy::prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update};
/// ){ /// ){
/// if keycode.just_pressed(KeyCode::Return) { /// if keycode.just_pressed(KeyCode::Return) {
/// commands.spawn(SceneBundle { /// commands.spawn(SceneBundle {
/// scene: asset_server.load("basic/models/level1.glb#Scene0"), /// scene: asset_server.load("basic/models/level1.glb"),
/// transform: Transform::from_xyz(2.0, 0.0, -5.0), /// transform: Transform::from_xyz(2.0, 0.0, -5.0),
/// ..Default::default() /// ..Default::default()
/// }); /// });
@ -48,11 +48,29 @@ pub enum GltfComponentsSet {
Injection, Injection,
} }
#[derive(Default)] #[derive(Clone, Resource)]
pub struct ComponentsFromGltfPlugin; pub struct GltfComponentsConfig {
pub(crate) legacy_mode: bool,
}
pub struct ComponentsFromGltfPlugin {
pub legacy_mode: bool
}
impl Default for ComponentsFromGltfPlugin {
fn default() -> Self {
Self {
legacy_mode: true
}
}
}
impl Plugin for ComponentsFromGltfPlugin { impl Plugin for ComponentsFromGltfPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.insert_resource(GltfLoadingTracker::new()) app.insert_resource(GltfLoadingTracker::new())
.insert_resource(GltfComponentsConfig {
legacy_mode: self.legacy_mode
})
.add_systems(Update, (track_new_gltf, process_loaded_scenes)) .add_systems(Update, (track_new_gltf, process_loaded_scenes))
.add_systems( .add_systems(
Update, Update,

View File

@ -4,20 +4,20 @@ use bevy::gltf::Gltf;
use bevy::utils::HashSet; use bevy::utils::HashSet;
use bevy::{asset::LoadState, prelude::*}; use bevy::{asset::LoadState, prelude::*};
use crate::gltf_extras_to_components; use crate::{gltf_extras_to_components, GltfComponentsConfig};
#[derive(Resource)] #[derive(Resource)]
/// component to keep track of gltfs' loading state /// component to keep track of gltfs' loading state
pub struct GltfLoadingTracker { pub struct GltfLoadingTracker {
pub loading_gltfs: HashSet<Handle<Gltf>>, pub loading_gltfs: HashSet<Handle<Gltf>>,
pub loaded_gltfs: HashSet<Handle<Gltf>>, pub processed_gltfs: HashSet<String>,
} }
impl GltfLoadingTracker { impl GltfLoadingTracker {
pub fn new() -> GltfLoadingTracker { pub fn new() -> GltfLoadingTracker {
GltfLoadingTracker { GltfLoadingTracker {
loaded_gltfs: HashSet::new(),
loading_gltfs: HashSet::new(), loading_gltfs: HashSet::new(),
processed_gltfs: HashSet::new(),
} }
} }
pub fn add_gltf(&mut self, handle: Handle<Gltf>) { pub fn add_gltf(&mut self, handle: Handle<Gltf>) {
@ -55,6 +55,7 @@ pub fn process_loaded_scenes(
mut scenes: ResMut<Assets<Scene>>, mut scenes: ResMut<Assets<Scene>>,
app_type_registry: Res<AppTypeRegistry>, app_type_registry: Res<AppTypeRegistry>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
gltf_components_config: Res<GltfComponentsConfig>,
) { ) {
let mut loaded_gltfs = Vec::new(); let mut loaded_gltfs = Vec::new();
for gltf in &tracker.loading_gltfs { for gltf in &tracker.loading_gltfs {
@ -75,10 +76,15 @@ pub fn process_loaded_scenes(
for gltf_handle in &loaded_gltfs { for gltf_handle in &loaded_gltfs {
if let Some(gltf) = gltfs.get_mut(gltf_handle) { if let Some(gltf) = gltfs.get_mut(gltf_handle) {
gltf_extras_to_components(gltf, &mut scenes, &*type_registry); gltf_extras_to_components(gltf, &mut scenes, &*type_registry, gltf_components_config.legacy_mode);
if let Some(path) = gltf_handle.path() {
tracker.processed_gltfs.insert(path.to_string());
}
} else {
warn!("could not find gltf asset, cannot process it");
} }
tracker.loading_gltfs.remove(gltf_handle); tracker.loading_gltfs.remove(gltf_handle);
tracker.loaded_gltfs.insert(gltf_handle.clone()); debug!("Done loading gltf file");
debug!("Done loading scene");
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bevy_gltf_save_load" name = "bevy_gltf_save_load"
version = "0.1.0" version = "0.2.0"
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"
@ -18,4 +18,4 @@ bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "
[dependencies] [dependencies]
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] } bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
bevy_gltf_blueprints = "0.6" bevy_gltf_blueprints = "0.7"

View File

@ -0,0 +1,21 @@
[package]
name = "bevy_registry_export"
version = "0.1.0"
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."
homepage = "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"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[dev-dependencies]
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
[dependencies]
bevy = { version = "0.12", default-features = false, features = ["bevy_scene"] }
bevy_reflect = { version = "0.12.1", default-features = false }
bevy_app = { version = "0.12.1", default-features = false, features = ["bevy_reflect"] }
bevy_ecs = { version = "0.12.1", default-features = false, features = ["bevy_reflect"] }
serde_json = "1.0.108"

View File

@ -0,0 +1,4 @@
This crate is available under either:
* The [MIT License](./LICENSE_MIT)
* The [Apache License, Version 2.0](./LICENSE_APACHE)

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2024] [Mark "kaosat-dev" Moissette]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Mark "kaosat-dev" Moissette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,130 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_registry_export)](https://crates.io/crates/bevy_registry_export)
[![Docs](https://img.shields.io/docsrs/bevy_registry_export)](https://docs.rs/bevy_registry_export/latest/bevy_registry_export/)
[![License](https://img.shields.io/crates/l/bevy_registry_export)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/bevy_registry_export/License.md)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
# bevy_registry_export
This plugin allows you to create a Json export of all your components/ registered types.
Its main use case is as a backbone for the [```bevy_components``` Blender add-on](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components), that allows you to add & edit components directly in Blender, using the actual type definitions from Bevy
(and any of your custom types & components that you register in Bevy).
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.12"
bevy_registry_export = "0.1"
```
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
ExportRegistryPlugin::default() // will save your registry schema json file to assets/registry.json
))
.run();
}
```
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
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_registry_export = "0.1"
```
Or use `cargo add`:
```toml
cargo add bevy_registry_export
```
## Setup
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
ExportRegistryPlugin::default()
))
.run();
}
```
you can also configure the output path
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
ExportRegistryPlugin {
save_path: "assets/registry.json".into(),
..Default::default()
},
))
.run();
}
```
## Usage
- The output file will be generated in the ```Startup``` schedule whenever you run your app.
- Every time you compile & run your app, the output json file will be updated.
## Examples
All examples are here:
> the examples use ```bevy_gltf_blueprints``` with the **legacy_mode** set to **FALSE** as the new custom properties generated by the Blender add-on require newer/ non legacy logic.
- https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/basic
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_registry_export` versions:
| `bevy_registry_export` | `bevy` | `bevy_components (Blender add-on)` |
| :-- | :-- |:-- |
| `0.1 ` | `0.12` | `0.1.0` |
| branch `main` | `0.12` | `0.1.0` |
| branch `bevy_main` | `main` | `n/a` |
## Contributors
Thanks to all the contributors helping out with this project ! Big kudos to you, contributions are always appreciated ! :)
A big shout out to [killercup](https://github.com/killercup), that did the bulk of the Bevy side code !
## License
This crate, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -0,0 +1,267 @@
use std::fs::File;
use bevy::log::info;
use bevy_ecs::{
reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
world::World,
};
use bevy_reflect::{TypeInfo, TypeRegistration, VariantInfo}; // TypePath // DynamicTypePath
use serde_json::{json, Map, Value};
use crate::ExportComponentsConfig;
pub fn export_types(world: &mut World) {
let config = world
.get_resource::<ExportComponentsConfig>()
.expect("ExportComponentsConfig should exist at this stage");
let writer =
File::create(config.save_path.to_path_buf()).expect("should have created schema file");
let types = world.resource_mut::<AppTypeRegistry>();
let types = types.read();
let schemas = types.iter().map(export_type).collect::<Map<_, _>>();
serde_json::to_writer_pretty(
writer,
&json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "bevy component registry schema",
"$defs": schemas,
}),
)
.expect("valid json");
info!("Done exporting registry schema")
}
pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
let t = reg.type_info();
let binding = t.type_path_table();
let short_name = binding.short_path();
let mut schema = match t {
TypeInfo::Struct(info) => {
let properties = info
.iter()
.enumerate()
.map(|(idx, field)| {
(
field.name().to_owned(),
add_min_max(json!({ "type": typ(field.type_path()) }), reg, idx, None),
)
})
.collect::<Map<_, _>>();
json!({
"type": "object",
"typeInfo": "Struct",
"title": t.type_path(),
"properties": properties,
"additionalProperties": false,
"required": info
.iter()
.filter(|field| !field.type_path().starts_with("core::option::Option"))
.map(|field| field.name())
.collect::<Vec<_>>(),
})
}
TypeInfo::Enum(info) => {
let simple = info
.iter()
.all(|variant| matches!(variant, VariantInfo::Unit(_)));
if simple {
json!({
"type": "string",
"typeInfo": "Enum",
"title": t.type_path(),
"oneOf": info
.iter()
.map(|variant| match variant {
VariantInfo::Unit(v) => v.name(),
_ => unreachable!(),
})
.collect::<Vec<_>>(),
})
} else {
let variants = info
.iter()
.enumerate()
.map(|(field_idx, variant)| match variant {
//let binding = t.type_path_table();
//let short_name = binding.short_path();
VariantInfo::Struct(v) => json!({
"type": "object",
"typeInfo": "Struct",
"title": v.name(),
"short_name": v.name().split("::").last().unwrap_or(v.name()),
"properties": v
.iter()
.enumerate()
.map(|(variant_idx, field)| (field.name().to_owned(), add_min_max(json!({"type": typ(field.type_path()), "title": field.name()}), reg, field_idx, Some(variant_idx))))
.collect::<Map<_, _>>(),
"additionalProperties": false,
"required": v
.iter()
.filter(|field| !field.type_path().starts_with("core::option::Option"))
.map(|field| field.name())
.collect::<Vec<_>>(),
}),
VariantInfo::Tuple(v) => json!({
"type": "array",
"typeInfo": "Tuple",
"title": v.name(),
"short_name":v.name(),
"prefixItems": v
.iter()
.enumerate()
.map(|(variant_idx, field)| add_min_max(json!({"type": typ(field.type_path())}), reg, field_idx, Some(variant_idx)))
.collect::<Vec<_>>(),
"items": false,
}),
VariantInfo::Unit(v) => json!({
"title": v.name(),
}),
})
.collect::<Vec<_>>();
json!({
"type": "object",
"typeInfo": "Enum",
"title": t.type_path(),
"oneOf": variants,
})
}
}
TypeInfo::TupleStruct(info) => json!({
"title": t.type_path(),
"type": "array",
"typeInfo": "TupleStruct",
"prefixItems": info
.iter()
.enumerate()
.map(|(idx, field)| add_min_max(json!({"type": typ(field.type_path())}), reg, idx, None))
.collect::<Vec<_>>(),
"items": false,
}),
TypeInfo::List(info) => {
json!({
"title": t.type_path(),
"type": "array",
"typeInfo": "List",
"items": json!({"type": typ(info.item_type_path_table().path())}),
})
}
TypeInfo::Array(info) => json!({
"title": t.type_path(),
"type": "array",
"typeInfo": "Array",
"items": json!({"type": typ(info.item_type_path_table().path())}),
}),
TypeInfo::Map(info) => json!({
"title": t.type_path(),
"type": "object",
"typeInfo": "Map",
"additionalProperties": json!({"type": typ(info.value_type_path_table().path())}),
}),
TypeInfo::Tuple(info) => json!({
"title": t.type_path(),
"type": "array",
"typeInfo": "Tuple",
"prefixItems": info
.iter()
.enumerate()
.map(|(idx, field)| add_min_max(json!({"type": typ(field.type_path())}), reg, idx, None))
.collect::<Vec<_>>(),
"items": false,
}),
TypeInfo::Value(info) => json!({
"title": t.type_path(),
"type": map_json_type(info.type_path()),
"typeInfo": "Value",
}),
};
schema.as_object_mut().unwrap().insert(
"isComponent".to_owned(),
reg.data::<ReflectComponent>().is_some().into(),
);
schema.as_object_mut().unwrap().insert(
"isResource".to_owned(),
reg.data::<ReflectResource>().is_some().into(),
);
schema
.as_object_mut()
.unwrap()
.insert("short_name".to_owned(), short_name.into());
(t.type_path().to_owned(), schema)
}
fn typ(t: &str) -> Value {
json!({ "$ref": format!("#/$defs/{t}") })
}
fn map_json_type(t: &str) -> Value {
match t {
"bool" => "boolean",
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" => "uint",
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" => "int",
"f32" | "f64" => "float",
"char" | "str" | "alloc::string::String" => "string",
_ => "object",
}
.into()
}
fn add_min_max(
mut val: Value,
reg: &TypeRegistration,
field_index: usize,
variant_index: Option<usize>,
) -> Value {
#[cfg(feature = "support-inspector")]
fn get_min_max(
reg: &TypeRegistration,
field_index: usize,
variant_index: Option<usize>,
) -> Option<(Option<f32>, Option<f32>)> {
use bevy_inspector_egui::inspector_options::{
std_options::NumberOptions, ReflectInspectorOptions, Target,
};
reg.data::<ReflectInspectorOptions>()
.and_then(|ReflectInspectorOptions(o)| {
o.get(if let Some(variant_index) = variant_index {
Target::VariantField {
variant_index,
field_index,
}
} else {
Target::Field(field_index)
})
})
.and_then(|o| o.downcast_ref::<NumberOptions<f32>>())
.map(|num| (num.min, num.max))
}
#[cfg(not(feature = "support-inspector"))]
fn get_min_max(
_reg: &TypeRegistration,
_field_index: usize,
_variant_index: Option<usize>,
) -> Option<(Option<f32>, Option<f32>)> {
None
}
let Some((min, max)) = get_min_max(reg, field_index, variant_index) else {
return val;
};
let obj = val.as_object_mut().unwrap();
if let Some(min) = min {
obj.insert("minimum".to_owned(), min.into());
}
if let Some(max) = max {
obj.insert("maximum".to_owned(), max.into());
}
val
}

View File

@ -0,0 +1,48 @@
pub mod export_types;
use std::path::PathBuf;
use bevy_app::Startup;
use bevy_ecs::system::Resource;
pub use export_types::*;
use bevy::{
prelude::{App, Plugin},
scene::SceneFilter,
};
// Plugin configuration
#[derive(Clone, Resource)]
pub struct ExportComponentsConfig {
pub(crate) save_path: PathBuf,
#[allow(dead_code)]
pub(crate) component_filter: SceneFilter, // unused for now
#[allow(dead_code)]
pub(crate) resource_filter: SceneFilter, // unused for now
}
pub struct ExportRegistryPlugin {
pub component_filter: SceneFilter,
pub resource_filter: SceneFilter,
pub save_path: PathBuf,
}
impl Default for ExportRegistryPlugin {
fn default() -> Self {
Self {
component_filter: SceneFilter::default(), // unused for now
resource_filter: SceneFilter::default(), // unused for now
save_path: PathBuf::from("assets/registry.json"),
}
}
}
impl Plugin for ExportRegistryPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(ExportComponentsConfig {
save_path: self.save_path.clone(),
component_filter: self.component_filter.clone(),
resource_filter: self.resource_filter.clone(),
})
.add_systems(Startup, export_types);
}
}

BIN
docs/bevy_components.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -34,7 +34,7 @@ fn main() {
RapierPhysicsPlugin::<NoUserData>::default(), RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(), RapierDebugRenderPlugin::default(),
// our custom plugins // our custom plugins
ComponentsFromGltfPlugin, ComponentsFromGltfPlugin::default(),
CorePlugin, // reusable plugins CorePlugin, // reusable plugins
DemoPlugin, // specific to our game DemoPlugin, // specific to our game
ComponentsTestPlugin, // Showcases different type of components /structs ComponentsTestPlugin, // Showcases different type of components /structs

View File

@ -34,7 +34,7 @@ fn main() {
RapierPhysicsPlugin::<NoUserData>::default(), RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(), RapierDebugRenderPlugin::default(),
// our custom plugins // our custom plugins
ComponentsFromGltfPlugin, ComponentsFromGltfPlugin::default(),
CorePlugin, // reusable plugins CorePlugin, // reusable plugins
DemoPlugin, // specific to our game DemoPlugin, // specific to our game
ComponentsTestPlugin, // Showcases different type of components /structs ComponentsTestPlugin, // Showcases different type of components /structs

View File

@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
bevy="0.12" bevy="0.12"
bevy_gltf_blueprints = "0.6" bevy_gltf_blueprints = "0.7"
bevy_gltf_save_load = { path = "../../../crates/bevy_gltf_save_load" } bevy_gltf_save_load = { path = "../../../crates/bevy_gltf_save_load" }
bevy_rapier3d = { version = "0.23.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] } bevy_rapier3d = { version = "0.23.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
[package]
name = "bevy_bevy_registry_export_basic_example"
version = "0.3.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { path = "../../../crates/bevy_gltf_blueprints" }
bevy_registry_export = { path = "../../../crates/bevy_registry_export" }
bevy_rapier3d = { version = "0.23.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }
bevy_asset_loader = { version = "0.18", features = ["standard_dynamic_assets" ]}
bevy_editor_pls = { version = "0.6" }
rand = "0.8.5"

View File

@ -0,0 +1,15 @@
# Bevy registry export example/demo
This example showcases
* the use of the bevy_registry_export crate to extract all components & types information into a json file.
* That file is then used by the [Blender addon](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components) to create Uis for each component,
to be able to add & edit Bevy components easilly in Blender !
## Running this example
```
cargo run --features bevy/dynamic_linking
```
Running the example also regenerates the registry.json file.

View File

@ -0,0 +1 @@
({})

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct CoreAssets {}

View File

@ -0,0 +1,13 @@
use bevy::gltf::Gltf;
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct GameAssets {
#[asset(key = "world")]
pub world: Handle<Gltf>,
#[asset(key = "models", collection(typed, mapped))]
pub models: HashMap<String, Handle<Gltf>>,
}

View File

@ -0,0 +1,35 @@
pub mod assets_core;
pub use assets_core::*;
pub mod assets_game;
pub use assets_game::*;
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
use crate::state::AppState;
pub struct AssetsPlugin;
impl Plugin for AssetsPlugin {
fn build(&self, app: &mut App) {
app
// load core assets (ie assets needed in the main menu, and everywhere else before loading more assets in game)
.add_loading_state(
LoadingState::new(AppState::CoreLoading).continue_to_state(AppState::MenuRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::CoreLoading,
"assets_core.assets.ron",
)
.add_collection_to_loading_state::<_, CoreAssets>(AppState::CoreLoading)
// load game assets
.add_loading_state(
LoadingState::new(AppState::AppLoading).continue_to_state(AppState::AppRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::AppLoading,
"assets_game.assets.ron",
)
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading);
}
}

View File

@ -0,0 +1,24 @@
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
use bevy::prelude::*;
use super::CameraTrackingOffset;
pub fn camera_replace_proxies(
mut commands: Commands,
mut added_cameras: Query<(Entity, &mut Camera), (Added<Camera>, With<CameraTrackingOffset>)>,
) {
for (entity, mut camera) in added_cameras.iter_mut() {
info!("detected added camera, updating proxy");
camera.hdr = true;
commands
.entity(entity)
.insert(DebandDither::Enabled)
.insert(Tonemapping::BlenderFilmic)
.insert(BloomSettings {
intensity: 0.01,
composite_mode: BloomCompositeMode::Additive,
..default()
});
}
}

View File

@ -0,0 +1,52 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
///
pub struct CameraTracking {
pub offset: Vec3,
}
impl Default for CameraTracking {
fn default() -> Self {
CameraTracking {
offset: Vec3::new(0.0, 6.0, 8.0),
}
}
}
#[derive(Component, Reflect, Debug, Deref, DerefMut)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
pub struct CameraTrackingOffset(Vec3);
impl Default for CameraTrackingOffset {
fn default() -> Self {
CameraTrackingOffset(Vec3::new(0.0, 6.0, 8.0))
}
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Add this component to an entity if you want it to be tracked by a Camera
pub struct CameraTrackable;
pub fn camera_track(
mut tracking_cameras: Query<
(&mut Transform, &CameraTrackingOffset),
(
With<Camera>,
With<CameraTrackingOffset>,
Without<CameraTrackable>,
),
>,
camera_tracked: Query<&Transform, With<CameraTrackable>>,
) {
for (mut camera_transform, tracking_offset) in tracking_cameras.iter_mut() {
for tracked_transform in camera_tracked.iter() {
let target_position = tracked_transform.translation + tracking_offset.0;
let eased_position = camera_transform.translation.lerp(target_position, 0.1);
camera_transform.translation = eased_position; // + tracking.offset;// tracked_transform.translation + tracking.offset;
*camera_transform = camera_transform.looking_at(tracked_transform.translation, Vec3::Y);
}
}
}

View File

@ -0,0 +1,24 @@
pub mod camera_tracking;
pub use camera_tracking::*;
pub mod camera_replace_proxies;
pub use camera_replace_proxies::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.register_type::<CameraTrackable>()
.register_type::<CameraTracking>()
.register_type::<CameraTrackingOffset>()
.add_systems(
Update,
(
camera_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
camera_track,
),
);
}
}

View File

@ -0,0 +1,25 @@
use bevy::prelude::*;
use bevy::pbr::{CascadeShadowConfig, CascadeShadowConfigBuilder};
// fixme might be too specific to might needs, should it be moved out ? also these are all for lights, not models
pub fn lighting_replace_proxies(
mut added_dirights: Query<(Entity, &mut DirectionalLight), Added<DirectionalLight>>,
mut added_spotlights: Query<&mut SpotLight, Added<SpotLight>>,
mut commands: Commands,
) {
for (entity, mut light) in added_dirights.iter_mut() {
light.illuminance *= 5.0;
light.shadows_enabled = true;
let shadow_config: CascadeShadowConfig = CascadeShadowConfigBuilder {
first_cascade_far_bound: 15.0,
maximum_distance: 135.0,
..default()
}
.into();
commands.entity(entity).insert(shadow_config);
}
for mut light in added_spotlights.iter_mut() {
light.shadows_enabled = true;
}
}

View File

@ -0,0 +1,18 @@
mod lighting_replace_proxies;
use lighting_replace_proxies::*;
use bevy::pbr::{DirectionalLightShadowMap, NotShadowCaster};
use bevy::prelude::*;
pub struct LightingPlugin;
impl Plugin for LightingPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(DirectionalLightShadowMap { size: 4096 })
// FIXME: adding these since they are missing
.register_type::<NotShadowCaster>()
.add_systems(PreUpdate, lighting_replace_proxies) // FIXME: you should actually run this in a specific state most likely
;
}
}

View File

@ -0,0 +1,38 @@
pub mod camera;
pub use camera::*;
pub mod lighting;
pub use lighting::*;
pub mod relationships;
pub use relationships::*;
pub mod physics;
pub use physics::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
use bevy_registry_export::*;
pub struct CorePlugin;
impl Plugin for CorePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExportRegistryPlugin {
save_path: "assets/registry.json".into(),
..Default::default()
},
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
BlueprintsPlugin {
legacy_mode: false,
library_folder: "models/library".into(),
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},
));
}
}

View File

@ -0,0 +1,12 @@
use bevy::{log::info, prelude::ResMut};
use bevy_rapier3d::prelude::RapierConfiguration;
pub fn pause_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("pausing physics");
physics_config.physics_pipeline_active = false;
}
pub fn resume_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("unpausing physics");
physics_config.physics_pipeline_active = true;
}

View File

@ -0,0 +1,37 @@
pub mod physics_replace_proxies;
use bevy_rapier3d::{
prelude::{NoUserData, RapierPhysicsPlugin},
render::RapierDebugRenderPlugin,
};
pub use physics_replace_proxies::*;
pub mod utils;
pub mod controls;
pub use controls::*;
use crate::state::GameState;
use bevy::prelude::*;
// use super::blueprints::GltfBlueprintsSet;
use bevy_gltf_blueprints::GltfBlueprintsSet;
// use crate::Collider;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.register_type::<AutoAABBCollider>()
.register_type::<physics_replace_proxies::Collider>()
// find a way to make serde's stuff serializable
// .register_type::<bevy_rapier3d::dynamics::CoefficientCombineRule>()
//bevy_rapier3d::dynamics::CoefficientCombineRule
.add_systems(
Update,
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
)
.add_systems(OnEnter(GameState::InGame), resume_physics)
.add_systems(OnExit(GameState::InGame), pause_physics);
}
}

View File

@ -0,0 +1,101 @@
use bevy::prelude::*;
// use bevy::render::primitives::Aabb;
use bevy_rapier3d::geometry::Collider as RapierCollider;
use bevy_rapier3d::prelude::{ActiveCollisionTypes, ActiveEvents, ComputedColliderShape};
use super::utils::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum Collider {
Ball(f32),
Cuboid(Vec3),
Capsule(Vec3, Vec3, f32),
#[default]
Mesh,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum AutoAABBCollider {
#[default]
Cuboid,
Ball,
Capsule,
}
// replaces all physics stand-ins with the actual rapier types
pub fn physics_replace_proxies(
meshes: Res<Assets<Mesh>>,
mesh_handles: Query<&Handle<Mesh>>,
mut proxy_colliders: Query<
(Entity, &Collider, &Name, &mut Visibility),
(Without<RapierCollider>, Added<Collider>),
>,
// needed for tri meshes
children: Query<&Children>,
mut commands: Commands,
) {
for proxy_colider in proxy_colliders.iter_mut() {
let (entity, collider_proxy, name, mut visibility) = proxy_colider;
// we hide the collider meshes: perhaps they should be removed altogether once processed ?
if name.ends_with("_collider") || name.ends_with("_sensor") {
*visibility = Visibility::Hidden;
}
let mut rapier_collider: RapierCollider;
match collider_proxy {
Collider::Ball(radius) => {
info!("generating collider from proxy: ball");
rapier_collider = RapierCollider::ball(*radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Cuboid(size) => {
info!("generating collider from proxy: cuboid");
rapier_collider = RapierCollider::cuboid(size.x, size.y, size.z);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Capsule(a, b, radius) => {
info!("generating collider from proxy: capsule");
rapier_collider = RapierCollider::capsule(*a, *b, *radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Mesh => {
info!("generating collider from proxy: mesh");
for (_, collider_mesh) in
Mesh::search_in_children(entity, &children, &meshes, &mesh_handles)
{
rapier_collider = RapierCollider::from_bevy_mesh(
collider_mesh,
&ComputedColliderShape::TriMesh,
)
.unwrap();
commands
.entity(entity)
.insert(rapier_collider)
// FIXME: this is just for demo purposes !!!
.insert(
ActiveCollisionTypes::default()
| ActiveCollisionTypes::KINEMATIC_STATIC
| ActiveCollisionTypes::STATIC_STATIC
| ActiveCollisionTypes::DYNAMIC_STATIC,
)
.insert(ActiveEvents::COLLISION_EVENTS);
// .insert(ActiveEvents::COLLISION_EVENTS)
// break;
// RapierCollider::convex_hull(points)
}
}
}
}
}

View File

@ -0,0 +1,175 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/src/util/trait_extension.rs
pub(crate) trait Vec3Ext: Copy {
fn is_approx_zero(self) -> bool;
fn split(self, up: Vec3) -> SplitVec3;
}
impl Vec3Ext for Vec3 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn split(self, up: Vec3) -> SplitVec3 {
let vertical = up * self.dot(up);
let horizontal = self - vertical;
SplitVec3 {
vertical,
horizontal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct SplitVec3 {
pub(crate) vertical: Vec3,
pub(crate) horizontal: Vec3,
}
pub(crate) trait Vec2Ext: Copy {
fn is_approx_zero(self) -> bool;
fn x0y(self) -> Vec3;
}
impl Vec2Ext for Vec2 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn x0y(self) -> Vec3 {
Vec3::new(self.x, 0., self.y)
}
}
pub(crate) trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
parent: Entity,
children: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)>;
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for coords in self.read_coords_mut(Mesh::ATTRIBUTE_POSITION.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
for normal in self.read_coords_mut(Mesh::ATTRIBUTE_NORMAL.clone()) {
let vec3 = (*normal).into();
let transformed = transform.rotation.mul_vec3(vec3);
*normal = transformed.into();
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
// Guaranteed by Bevy for the current usage
match self
.attribute_mut(id)
.expect("Failed to read unknown mesh attribute")
{
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy for the current usage
_ => unreachable!(),
}
}
fn search_in_children<'a>(
parent: Entity,
children_query: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)> {
if let Ok(children) = children_query.get(parent) {
let mut result: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.map(|(entity, mesh_handle)| {
(
entity,
meshes
.get(mesh_handle)
.expect("Failed to get mesh from handle"),
)
})
.map(|(entity, mesh)| {
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(entity, mesh)
})
.collect();
let mut inner_result = children
.iter()
.flat_map(|entity| {
Self::search_in_children(*entity, children_query, meshes, mesh_handles)
})
.collect();
result.append(&mut inner_result);
result
} else {
Vec::new()
}
}
}
pub(crate) trait F32Ext: Copy {
fn is_approx_zero(self) -> bool;
fn squared(self) -> f32;
fn lerp(self, other: f32, ratio: f32) -> f32;
}
impl F32Ext for f32 {
#[inline]
fn is_approx_zero(self) -> bool {
self.abs() < 1e-5
}
#[inline]
fn squared(self) -> f32 {
self * self
}
#[inline]
fn lerp(self, other: f32, ratio: f32) -> f32 {
self.mul_add(1. - ratio, other * ratio)
}
}
pub(crate) trait TransformExt: Copy {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform;
fn lerp(self, other: Transform, ratio: f32) -> Transform;
}
impl TransformExt for Transform {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform {
let direction = target - self.translation;
let horizontal_direction = direction - up * direction.dot(up);
let look_target = self.translation + horizontal_direction;
self.looking_at(look_target, up)
}
fn lerp(self, other: Transform, ratio: f32) -> Transform {
let translation = self.translation.lerp(other.translation, ratio);
let rotation = self.rotation.slerp(other.rotation, ratio);
let scale = self.scale.lerp(other.scale, ratio);
Transform {
translation,
rotation,
scale,
}
}
}

View File

@ -0,0 +1,75 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/6e31fc02652fc9d085a4adde0a73ab007dbbb0dc/src/util/trait_extension.rs
pub trait Vec3Ext {
#[allow(clippy::wrong_self_convention)] // Because [`Vec3`] is [`Copy`]
fn is_approx_zero(self) -> bool;
fn x0z(self) -> Vec3;
}
impl Vec3Ext for Vec3 {
fn is_approx_zero(self) -> bool {
[self.x, self.y, self.z].iter().all(|&x| x.abs() < 1e-5)
}
fn x0z(self) -> Vec3 {
Vec3::new(self.x, 0., self.z)
}
}
pub trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh);
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for attribute in [Mesh::ATTRIBUTE_POSITION, Mesh::ATTRIBUTE_NORMAL] {
for coords in self.read_coords_mut(attribute.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
match self.attribute_mut(id).unwrap() {
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy
_ => unreachable!(),
}
}
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh) {
let entity_handles: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.collect();
assert_eq!(
entity_handles.len(),
1,
"Collider must contain exactly one mesh, but found {}",
entity_handles.len()
);
let (entity, mesh_handle) = entity_handles.first().unwrap();
let mesh = meshes.get(mesh_handle).unwrap();
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(*entity, mesh)
}
}

View File

@ -0,0 +1,11 @@
pub mod relationships_insert_dependant_components;
pub use relationships_insert_dependant_components::*;
use bevy::prelude::*;
pub struct EcsRelationshipsPlugin;
impl Plugin for EcsRelationshipsPlugin {
fn build(&self, app: &mut App) {
app;
}
}

View File

@ -0,0 +1,15 @@
use bevy::prelude::*;
pub fn insert_dependant_component<
Dependant: Component,
Dependency: Component + std::default::Default,
>(
mut commands: Commands,
entities_without_depency: Query<(Entity, &Name), (With<Dependant>, Without<Dependency>)>,
) {
for (entity, name) in entities_without_depency.iter() {
let name = name.clone().to_string();
commands.entity(entity).insert(Dependency::default());
warn!("found an entity called {} with a {} component but without an {}, please check your assets", name.clone(), std::any::type_name::<Dependant>(), std::any::type_name::<Dependency>());
}
}

View File

@ -0,0 +1,132 @@
use bevy::prelude::*;
use crate::{
assets::GameAssets,
state::{GameState, InAppRunning},
};
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag};
use bevy_rapier3d::prelude::Velocity;
use rand::Rng;
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>,
models: Res<Assets<bevy::gltf::Gltf>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
println!("setting up all stuff");
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.2,
});
// here we actually spawn our game world/level
commands.spawn((
SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
next_game_state.set(GameState::InGame)
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct UnregisteredComponent;
pub fn spawn_test(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
) {
if keycode.just_pressed(KeyCode::T) {
let world = game_world.single_mut();
let world = world.1[0];
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
))
.id();
commands.entity(world).add_child(new_entity);
}
}
pub fn spawn_test_unregisted_components(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
) {
if keycode.just_pressed(KeyCode::U) {
let world = game_world.single_mut();
let world = world.1[0];
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
UnregisteredComponent,
))
.id();
commands.entity(world).add_child(new_entity);
}
}

View File

@ -0,0 +1,113 @@
use bevy::prelude::*;
use crate::state::{AppState, InMainMenu};
pub fn setup_main_menu(mut commands: Commands) {
commands.spawn((Camera2dBundle::default(), InMainMenu));
commands.spawn((
TextBundle::from_section(
"SOME GAME TITLE !!",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(100.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
commands.spawn((
TextBundle::from_section(
"New Game (press Enter to start, press T once the game is started for demo spawning)",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(200.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
/*
commands.spawn((
TextBundle::from_section(
"Load Game",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(250.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));
commands.spawn((
TextBundle::from_section(
"Exit Game",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(300.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));*/
}
pub fn teardown_main_menu(bla: Query<Entity, With<InMainMenu>>, mut commands: Commands) {
for bli in bla.iter() {
commands.entity(bli).despawn_recursive();
}
}
pub fn main_menu(
keycode: Res<Input<KeyCode>>,
mut next_app_state: ResMut<NextState<AppState>>,
// mut next_game_state: ResMut<NextState<GameState>>,
// mut save_requested_events: EventWriter<SaveRequest>,
// mut load_requested_events: EventWriter<LoadRequest>,
) {
if keycode.just_pressed(KeyCode::Return) {
next_app_state.set(AppState::AppLoading);
// next_game_state.set(GameState::None);
}
if keycode.just_pressed(KeyCode::L) {
next_app_state.set(AppState::AppLoading);
// load_requested_events.send(LoadRequest { path: "toto".into() })
}
if keycode.just_pressed(KeyCode::S) {
// save_requested_events.send(SaveRequest { path: "toto".into() })
}
}

View File

@ -0,0 +1,90 @@
pub mod in_game;
pub use in_game::*;
pub mod in_main_menu;
pub use in_main_menu::*;
pub mod picking;
pub use picking::*;
use crate::state::{AppState, GameState};
use bevy::prelude::*;
// this file is just for demo purposes, contains various types of components, systems etc
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum SoundMaterial {
Metal,
Wood,
Rock,
Cloth,
Squishy,
#[default]
None,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Player;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo component showing auto injection of components
pub struct ShouldBeWithPlayer;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Interactible;
fn player_move_demo(
keycode: Res<Input<KeyCode>>,
mut players: Query<&mut Transform, With<Player>>,
) {
let speed = 0.2;
if let Ok(mut player) = players.get_single_mut() {
if keycode.pressed(KeyCode::Left) {
player.translation.x += speed;
}
if keycode.pressed(KeyCode::Right) {
player.translation.x -= speed;
}
if keycode.pressed(KeyCode::Up) {
player.translation.z += speed;
}
if keycode.pressed(KeyCode::Down) {
player.translation.z -= speed;
}
}
}
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(PickingPlugin)
.register_type::<Interactible>()
.register_type::<SoundMaterial>()
.register_type::<Player>()
// little helper utility, to automatically inject components that are dependant on an other component
// ie, here an Entity with a Player component should also always have a ShouldBeWithPlayer component
// you get a warning if you use this, as I consider this to be stop-gap solution (usually you should have either a bundle, or directly define all needed components)
.add_systems(
Update,
(
// insert_dependant_component::<Player, ShouldBeWithPlayer>,
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events
spawn_test,
spawn_test_unregisted_components,
)
.run_if(in_state(GameState::InGame)),
)
.add_systems(OnEnter(AppState::MenuRunning), setup_main_menu)
.add_systems(OnExit(AppState::MenuRunning), teardown_main_menu)
.add_systems(Update, main_menu.run_if(in_state(AppState::MenuRunning)))
.add_systems(OnEnter(AppState::AppRunning), setup_game);
}
}

View File

@ -0,0 +1,34 @@
use super::Player;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct Pickable;
// very simple, crude picking (as in picking up objects) implementation
pub fn picking(
players: Query<&GlobalTransform, With<Player>>,
pickables: Query<(Entity, &GlobalTransform), With<Pickable>>,
mut commands: Commands,
) {
for player_transforms in players.iter() {
for (pickable, pickable_transforms) in pickables.iter() {
let distance = player_transforms
.translation()
.distance(pickable_transforms.translation());
if distance < 2.5 {
commands.entity(pickable).despawn_recursive();
}
}
}
}
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Pickable>()
.add_systems(Update, (picking.after(GltfBlueprintsSet::AfterSpawn),));
}
}

View File

@ -0,0 +1,33 @@
use bevy::prelude::*;
use bevy_editor_pls::prelude::*;
mod core;
use crate::core::*;
pub mod assets;
use assets::*;
pub mod state;
use state::*;
mod game;
use game::*;
mod test_components;
use test_components::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(AssetPlugin::default()),
// editor
EditorPlugin::default(),
// our custom plugins
StatePlugin,
AssetsPlugin,
CorePlugin, // reusable plugins
GamePlugin, // specific to our game
ComponentsTestPlugin, // Showcases different type of components /structs
))
.run();
}

View File

@ -0,0 +1,54 @@
use bevy::app::AppExit;
use bevy::prelude::*;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum AppState {
#[default]
CoreLoading,
MenuRunning,
AppLoading,
AppRunning,
AppEnding,
// FIXME: not sure
LoadingGame,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum GameState {
#[default]
None,
InMenu,
InGame,
InGameOver,
InSaving,
InLoading,
}
// tag components for all entities within a certain state (for despawning them if needed) , FIXME: seems kinda hack-ish
#[derive(Component)]
pub struct InCoreLoading;
#[derive(Component, Default)]
pub struct InMenuRunning;
#[derive(Component)]
pub struct InAppLoading;
#[derive(Component)]
pub struct InAppRunning;
// components for tagging in game vs in game menu stuff
#[derive(Component, Default)]
pub struct InMainMenu;
#[derive(Component, Default)]
pub struct InMenu;
#[derive(Component, Default)]
pub struct InGame;
pub struct StatePlugin;
impl Plugin for StatePlugin {
fn build(&self, app: &mut App) {
app.add_state::<AppState>().add_state::<GameState>();
}
}

View File

@ -0,0 +1,136 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct UnitTest;
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
struct TuppleTestF32(f32);
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
struct TuppleTestU64(u64);
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
pub struct TuppleTestStr(String);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTest2(f32, u64, String);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTestBool(bool);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec2(Vec2);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec3(Vec3);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec(Vec<String>);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVecF32F32(Vec<(f32, f32)>);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTestColor(Color);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct BasicTest {
a: f32,
b: u64,
c: String,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum EnumTest {
Metal,
Wood,
Rock,
Cloth,
Squishy,
#[default]
None,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct NestingTestLevel2 {
text: String,
enable: bool,
enum_inner: EnumTest,
color: TuppleTestColor,
toggle: TuppleTestBool,
basic: BasicTest,
pub nested: NestingTestLevel3,
colors_list: VecOfColors,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct NestingTestLevel3 {
vec: TuppleVec3,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct NestedTuppleStuff(f32, u64, NestingTestLevel2);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum EnumComplex {
Float(f32),
Wood(String),
Vec(BasicTest),
SomeThing,
#[default]
None,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct VecOfVec3s2(Vec<TuppleVec3>);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct VecOfColors(Vec<Color>);
pub struct ComponentsTestPlugin;
impl Plugin for ComponentsTestPlugin {
fn build(&self, app: &mut App) {
app.register_type::<BasicTest>()
.register_type::<UnitTest>()
.register_type::<TuppleTestF32>()
.register_type::<TuppleTestU64>()
.register_type::<TuppleTestStr>()
.register_type::<TuppleTestBool>()
.register_type::<TuppleTest2>()
.register_type::<TuppleVec2>()
.register_type::<TuppleVec3>()
.register_type::<EnumTest>()
.register_type::<TuppleTestColor>()
.register_type::<TuppleVec>()
.register_type::<Vec<String>>()
.register_type::<NestingTestLevel2>()
.register_type::<NestingTestLevel3>()
.register_type::<NestedTuppleStuff>()
.register_type::<EnumComplex>()
.register_type::<VecOfVec3s2>()
.register_type::<TuppleVecF32F32>()
.register_type::<(f32, f32)>()
.register_type::<Vec<(f32, f32)>>()
.register_type::<Vec<TuppleVec3>>()
.register_type::<Vec<Color>>()
.register_type::<VecOfColors>();
}
}

View File

@ -0,0 +1,199 @@
# Bevy components
This [Blender addon](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components) gives you an easy to use UI to add and configure your [Bevy](https://bevyengine.org/) components inside Blender !
- **automatically generates a simple UI** to add/configure components based on a **registry schema** file (an export of all your Bevy components's information, generated)
by the [bevy_registry_export](https://crates.io/crates/bevy_registry_export) crate/plugin
- no more need to specify components manually using custom_properties, with error prone naming etc
- adds **metadata** to objects containing information about what components it uses + some extra information
- uses Blender's **PropertyGroups** to generate custom UIs & connects those groups with the custom properties so that no matter the complexity
of your Bevy components you get a nicely packed custom_property to use with ...
- the ideal companion to the [gltf_auto_export](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export) to embed your Bevy components inside your gltf files
<!-- - adds the ability to **toggle components** on/off without having to remove the component from the object -->
> Important:
the tooling is still in the early stages, even if it is feature complete : use with caution!.
## Installation:
* grab the latest release zip file from the releases tab (choose the bevy_components releases !)
<!--![blender addon install](./docs/blender_addon_install_zip.png)-->
* in Blender go to edit => preferences => install
![blender addon install](./docs/blender_addon_install.png)
* choose the path where ```bevy_components.zip``` is stored
![blender addon install](./docs/blender_addon_install2.png)
## Configuration & overview
Before you can use the add-on you need to configure it
### Bevy side
- setup [bevy_registry_export](https://crates.io/crates/bevy_registry_export) for your project (see the crate's documentation for that), and compile/run it to get the ```registry.json``` file
### Blender side
- Go to the new Bevy Components tab in the 3D view
![configuration](./docs/configuration.png)
- click on the button to select your registry.json file (in the "configuration" panel)
![configuration 2](./docs/configuration2.png)
- the list of available components will appear
![configuration 3](./docs/configuration3.png)
## Use
### Existing components & custom properties
* If you already have components defined manualy in Blender inside **custom properties** you will need to define them again using the UI!
* avoid mixing & matching: if you change the values of **custom properties** that also have a component, the custom property will be **overriden** every time
you change the component's value
* you can of course still use non component custom properties as always, this add-on will only impact those that have corresponding Bevy components
### adding components
- to add a component, select an object and then select the component from the components list: (the full type information will be displayed as tooltip)
![components list](./docs/components_list.png)
- click on the dropdown to get the full list of available components
![components list](./docs/components_list2.png)
- you can also filter components by name for convenience
![filter components](./docs/filter_components.png)
- add a component by clicking on the "add component" button once you have selected your desired component
it will appear in the component list for that object
![add component](./docs/add_component.png)
### edit components
- to edit a component's value just use the UI:
![edit component](./docs/edit_component.png)
it will automatically update the value of the corresponding custom property
![edit component](./docs/edit_component2.png)
### copy & pasting
- you can also copy & paste components between objects
- click on the "copy component button" of the component you want to copy
![copy component](./docs/copy_component.png)
- then select the object you want to copy the component (& its value) to, and click on the paste button.
It will add the component to the select object
![paste component](./docs/paste_component.png)
> if the target object already has the same component, its values will be overwritten
## Additional components UI features
- for large/ complex components you can toggle the details of that component:
![toggle details](./docs/toggle_details.png)
## Supported components
- normally (minus any bugs, please report those!) all components using **registered** types should be useable and editable
- this includes (non exhaustive list):
* enums (even complex ones !)
![enums](./docs/enums.png)
![enums](./docs/enums2.png)
* complex structs, with various types of fields (including nested ones)
![complex](./docs/complex_components2.png)
* lists/ vecs (here a vec of tuples)
![lists](./docs/vecs_lists.png)
* etc !
## Unregistered types & error handling
- non registered types can be viewed in this panel : (can be practical to see if you have any missing registrations too!)
![unregistered types](./docs/unregistered_types.png)
- if you have a component made up of unregistered structs/enums etc, you will get visual feedback & the component will be deactivated
![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
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
![missing registry data](./docs/missing_registry_data.png)
## advanced configuration
- there are also additional QOL features, that you should not need most of the time
- "update custom properties of current object" : will go over **all components** that you have defined for the **currently selected object**, and re-generate the
corresponding custom property values
![update custom properties](./docs/other_options.png)
- "update custom properties of ALL objects" : same as above but it will do so for the **ALL objects in your blend file** (so can be slow!), and re-generate the
corresponding custom property values
![update custom properties for all](./docs/other_options2.png)
## Additional important information
- for the components to work correctly with [```bevy_gltf_components```](https://crates.io/crates/bevy_gltf_components) or [```bevy_gltf_blueprints```](https://crates.io/crates/bevy_gltf_blueprints) you will need to set the ```legacy_mode``` for those plugins to **FALSE**
as the component data generated by this add on is a complete, clean **ron** data that is incompatible with the previous (legacy versions).
Please see the documentation of those crates for more information.
> Note: the legacy mode support will be dropped in future versions, and the default behaviour will be NO legacy mode
## Examples
you can find an example [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/)
## License
This tool, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](../LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -0,0 +1,139 @@
Basics
- [x] add panel
- [x] add a "create blueprint" button
- [x] when clicked:
- [x] create collection
- [x] add an empty inside collection and name it <COLLECTION_NAME>_components
- [x] add a **AutoExport** Boolean property to collection
- [x] add name imput(popup for name input ?)
- [x] add a list of existing components/custom properties
- [x] add an "edit blueprint" section
- [x] only filled when there is ONE selection, and that selection is a collection
- [x] add a dropdown of possible components
- [x] add a checkbox for enabling disabling a component (enabled by default)
- [x] add a button for copying a component
- [x] add a button for pasting a component
UI:
- [x] filterable list of components to DISPLAY for selection : ComponentDefinitionsList
- Filter out invalid objects for components that have no _components suffix ? (that is too limiting I think)
- -[x] How to deal with pre-existing custom properties that have NO metadata
* if there is one without metadata: find if there is an available component with the same name & type ?
* if there is , insert metadata
* otherwise, mark it in some way visually ?
- [x] for OBJECT enums: add two ui pieces
- [x] one for selecting the TYPE to choose (ie normal enum)
- [x] one for setting the VALUE inside that
- [x] vecs => (not vec2, vec3 etc) more complex UI to add items in a list
- [x] generate contained CollectionGroup
- [x] CollectionProperty => type = the above
- [x] find ways to "collapse" the different levels of nested data of structs/tupples into a single custom property (ideally on the fly, but we can do without)
- [x] for single tupple components that represent a single unit type, re_use the base type's UIPropertyGroup instead of creating specific ones (ie TuppleTestF32_ui...) => will not work, would cause overriden "update callback"
- [x] pre_generate default values/values for each main type
- [x] fix issues with vec2 etc not having the correct number of items
- [x] fix bad defaults in ui group
- [x] fix object enums handling on updates (??)
- [x] fix issues with lambads in loops
- [x] object enum should be <EntryName>(params)
ie *Collider:
* Cuboid(Vec3)
* Sphere(radius)
- [x] deal with enums variants that do not have any data: ex {
"title": "Mesh"
}
- [x] remove / change use of ComponentDefinitionsList
- when filling the list, use the long_name as index ie items.append((str(index), item.name, item.long_name)) => items.append((item.long_name, item.name, item.long_name))
- [x] when removing a component, reset the value of the attribute in the property group (or not ? could be a feature)
- [x] deal correctly with fields of types that are NOT in the schema.json (for ex PlayingAnimation in AnimationPlayer)
- [ ] deal correctly with complex types
CascadeShadowConfig: has an array/list
ClusterConfig: one of the enum variants is an object
- [ ] possibly allow Color to be an enum as it should be ?
- [x] for sub items , the update functions "Name" should be the one of the root object
- [x] fix copy & pasting
- it actually works, but the value of the custom property are not copied back to the UI, need to implement property_group_value_from_custom_property_value
- [ ] we need a notion of "root propertyGroup" =?
- [x] notify user of missing entries in schema (ie , unregistered data types)
- [x] clarify propgroup_ui vs named nested fields
- [x] fix basic enums handling
- [x] add a list of not found components to the registry, add to them on the fly
- [x] add configuration panel (open the first time, closed on further user once configured)
- [x] add limits to ixxx types vs utypes
- [x] only display the "generate components xx" when relevant ie:
- go through list of custom properties in current object
- if one does not have metadata and / or propgroup:
break
- [x] remove custom property of disabled component ? => NOpe, as we need custom properties to iterate over
- [x] what to do with components with n/a fields ? perhaps disable the component ? add a "invalid" field to meta ?
- [x] format output as correct RON
- [x] fix issue with empty strings
- [x] change custom property => propGroup to convert RON => Json first => obsolete
- [x] cleanup process_lists
- [x] fix issues with enum variants with only a title
- [x] display single item enums inline, others in a seperate row
- [x] add button to "apply all" (in configuration), to apply/update all custom properties to ALL objects where relevant
- [x] add button to "apply to current" to do the same with current
- [x] add warning sign to the above
- [x] what about metadata ?
- [x] only upgrade custom properties to metadata when asked/relevant
- [x] implement move list up/down
- [ ] change property_group_value_from_custom_property_value => just disregard it for now, its point is very limited (helping people with old custom properties by attempting to generate real values)
and give the change to a real ron format, it is too limiting
- [x] fix reload registry clearing list of missing types
- [x] clean up metadata module, a lot of repeated code
- [x] some fields when original is 0 or 0.0 are not copyable ? (seems like a bad boolean check )
- [x] fix issues with object variants in enums (see clusterconfig)
- perhaps directly export default values within the schema.json ?
- for most types , it is straighforward, but others, not so much: like the default color in Bevy , etc
- [x] change default schema.json to registry.json
- [x] pasted components do not get updated value in custom_property
- [x] finish documentation
- [x] add storage of registry path
- [x] save after setting the data (browse for)
- [x] load after each reload ?
# Additional
- [x] check if output "string" in custom properties are correct
- gltf_auto_export
- [ ] add support for "enabled" flag
- [ ] add special components
- "AutoExport" => Needed
- "Dynamic" ? naah wait that should be exported by the Bevy side
- [ ] filter out Components_meta ??
- [x] add legacy mode to the persisted parameters
- bevy_gltf_components:
- [x] first release patch for current issues
- [x] make configurable
- [x] add "compatibility mode" and deprecation warnings for the current hack-ish conversion of fake ron
- [x] update docs to show we need to use ComponentsFromGltfPlugin::default
- bevy_gltf_blueprints
- [x] update dependency
- [x] update version
- [x] add ability to set legacy mode for bevy_gltf_components ?
- [ ] release all versions
- [ ] update main documentation, add compatibility version grid

View File

@ -0,0 +1,143 @@
bl_info = {
"name": "bevy_components",
"author": "kaosigh",
"version": (0, 1, 0),
"blender": (3, 4, 0),
"location": "VIEW_3D",
"description": "UI to help create Bevy blueprints and components",
"warning": "",
"wiki_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow",
"tracker_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow/issues/new",
"category": "User Interface"
}
import bpy
from bpy.props import (StringProperty)
from .helpers import load_settings
from .blueprints import CreateBlueprintOperator
from .components.operators import CopyComponentOperator, DeleteComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, Toggle_ComponentVisibility
from .registry.registry import ComponentsRegistry,MissingBevyType
from .registry.operators import (COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT, ReloadRegistryOperator, OT_OpenFilebrowser)
from .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List)
from .components.metadata import (ComponentInfos, ComponentsMeta, ensure_metadata_for_all_objects)
from .propGroups.prop_groups import (generate_propertyGroups_for_components)
from .components.lists import GENERIC_LIST_OT_actions, Generic_LIST_OT_AddItem, Generic_LIST_OT_RemoveItem, Generic_LIST_OT_SelectItem
from .components.definitions_list import (ComponentDefinitionsList, ClearComponentDefinitionsList)
from .components.ui import (BEVY_COMPONENTS_PT_ComponentsPanel)
# just a test, remove
def scan_item(item, nesting=0):
try:
for sub in dict(item).keys():
print("--", sub, getattr(item[sub], "type_name", None), item[sub], nesting)
try:
scan_item(item[sub], nesting+1)
except:
pass
except:
pass
class BEVY_COMPONENTS_PT_MainPanel(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_MainPanel"
bl_label = ""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
def draw_header(self, context):
layout = self.layout
name = context.object.name if context.object != None else ''
layout.label(text="Components For "+ name)
def draw(self, context):
layout = self.layout
object = context.object
collection = context.collection
"""row.prop(bpy.context.window_manager, "blueprint_name")
op = row.operator(CreateBlueprintOperator.bl_idname, text="Create blueprint", icon="CONSOLE")
op.blueprint_name = bpy.context.window_manager.blueprint_name
layout.separator()
current_components_container = None
has_components = False
for child in collection.objects:
if child.name.endswith("_components"):
has_components = True
current_components_container= child
if collection is not None and has_components:
layout.label(text="Edit blueprint: "+ collection.name)
"""
#_register, _unregister = bpy.utils.register_classes_factory(classes)
classes = [
CreateBlueprintOperator,
AddComponentOperator,
CopyComponentOperator,
PasteComponentOperator,
DeleteComponentOperator,
GenerateComponent_From_custom_property_Operator,
Toggle_ComponentVisibility,
ComponentDefinitionsList,
ClearComponentDefinitionsList,
ComponentInfos,
ComponentsMeta,
MissingBevyType,
ComponentsRegistry,
OT_OpenFilebrowser,
ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT,
BEVY_COMPONENTS_PT_MainPanel,
BEVY_COMPONENTS_PT_ComponentsPanel,
BEVY_COMPONENTS_PT_Configuration,
MISSING_TYPES_UL_List,
BEVY_COMPONENTS_PT_MissingTypesPanel,
Generic_LIST_OT_SelectItem,
Generic_LIST_OT_AddItem,
Generic_LIST_OT_RemoveItem,
GENERIC_LIST_OT_actions
]
from bpy.app.handlers import persistent
@persistent
def post_load(file_name):
print("post load", file_name)
registry = bpy.context.window_manager.components_registry
registry.schemaPath = load_settings(registry.settings_save_path)["schemaPath"]
registry.load_schema()
generate_propertyGroups_for_components()
ensure_metadata_for_all_objects()
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.WindowManager.blueprint_name = StringProperty()
bpy.app.handlers.load_post.append(post_load)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.WindowManager.blueprint_name
bpy.app.handlers.load_post.remove(post_load)

View File

@ -0,0 +1,41 @@
import json
import bpy
from bpy_types import Operator
from bpy.props import (StringProperty)
from .helpers import make_empty
class CreateBlueprintOperator(Operator):
"""Creates blueprint"""
bl_idname = "object.simple_operator"
bl_label = "Simple Object Operator"
blueprint_name: StringProperty(
name="blueprint name",
description="blueprint name to add",
default="NewBlueprint"
)
def execute(self, context):
blueprint_name = self.blueprint_name
if blueprint_name == '':
blueprint_name = "NewBlueprint"
collection = bpy.data.collections.new(blueprint_name)
bpy.context.scene.collection.children.link(collection)
collection['AutoExport'] = True
# this is in order to deal with automatic naming
blueprint_name = collection.name
components_empty = make_empty(blueprint_name + "_components", [0,0,0], [0,0,0], [0,0,0], bpy.context.scene.collection)
bpy.ops.collection.objects_remove_all()
collection.objects.link(components_empty)
components_empty.select_set(True)
bpy.context.view_layer.objects.active = components_empty
return {'FINISHED'}

View File

@ -0,0 +1,56 @@
import bpy
from bpy.props import (StringProperty)
# this one is for UI only, and its inner list contains a useable list of shortnames of components
class ComponentDefinitionsList(bpy.types.PropertyGroup):
# FIXME: not sure, hard coded exclude list, feels wrong
exclude = ['Parent', 'Children']
def add_component_to_ui_list(self, context):
#print("add components to ui_list")
items = []
type_infos = context.window_manager.components_registry.type_infos
short_names = context.window_manager.components_registry.short_names_to_long_names
for short_name in sorted(short_names.keys()):
long_name = short_names[short_name]
definition = type_infos[long_name]
is_component = definition['isComponent'] if "isComponent" in definition else False
if self.filter in short_name and is_component:
if not 'Handle' in short_name and not "Cow" in short_name and not "AssetId" in short_name and short_name not in self.exclude: # FIXME: hard coded, seems wrong
items.append((long_name, short_name, long_name))
return items
@classmethod
def register(cls):
bpy.types.WindowManager.components_list = bpy.props.PointerProperty(type=ComponentDefinitionsList)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_list
list : bpy.props.EnumProperty(
name="list",
description="list",
# items argument required to initialize, just filled with empty values
items = add_component_to_ui_list,
)
filter: StringProperty(
name="component filter",
description="filter for the components list",
options={'TEXTEDIT_UPDATE'}
)
class ClearComponentDefinitionsList(bpy.types.Operator):
''' clear list of bpy.context.collection.component_definitions '''
bl_label = "clear component definitions"
bl_idname = "components.clear_component_definitions"
def execute(self, context):
# create a new item, assign its properties
bpy.context.collection.component_definitions.clear()
return {'FINISHED'}

View File

@ -0,0 +1,170 @@
import json
from bpy_types import Operator, UIList
from bpy.props import (StringProperty, EnumProperty, PointerProperty, FloatVectorProperty, IntProperty)
class Generic_LIST_OT_AddItem(Operator):
"""Add a new item to the list."""
bl_idname = "generic_list.add_item"
bl_label = "Add a new item"
property_group_path: StringProperty(
name="property group path",
description="",
)
component_name: StringProperty(
name="component name",
description="",
)
def execute(self, context):
print("")
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
print("list container", propertyGroup, dict(propertyGroup))
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
item = target_list.add()
propertyGroup.list_index = index + 1 # we use this to force the change detection
print("added item", item, item.field_names, getattr(item, "field_names"))
print("")
return{'FINISHED'}
class Generic_LIST_OT_RemoveItem(Operator):
"""Remove an item to the list."""
bl_idname = "generic_list.remove_item"
bl_label = "Remove selected item"
property_group_path: StringProperty(
name="property group path",
description="",
)
component_name: StringProperty(
name="component name",
description="",
)
def execute(self, context):
print("remove from list", context.object)
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
target_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(target_list) - 1)
return{'FINISHED'}
class Generic_LIST_OT_SelectItem(Operator):
"""Remove an item to the list."""
bl_idname = "generic_list.select_item"
bl_label = "select an item"
property_group_path: StringProperty(
name="property group path",
description="",
)
component_name: StringProperty(
name="component name",
description="",
)
selection_index: IntProperty()
def execute(self, context):
print("select in list", context.object)
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
propertyGroup.list_index = self.selection_index
return{'FINISHED'}
class GENERIC_LIST_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "generic_list.list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER', 'UNDO'}
action: EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", "")))
property_group_path: StringProperty(
name="property group path",
description="",
)
component_name: StringProperty(
name="component name",
description="",
)
def invoke(self, context, event):
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
if self.action == 'DOWN' and index < len(target_list) - 1:
#item_next = scn.rule_list[index + 1].name
target_list.move(index, index + 1)
propertyGroup.list_index += 1
elif self.action == 'UP' and index >= 1:
#item_prev = scn.rule_list[index - 1].name
target_list.move(index, index - 1)
propertyGroup.list_index -= 1
elif self.action == 'REMOVE':
target_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(target_list) - 1)
if self.action == 'ADD':
item = target_list.add()
propertyGroup.list_index = index + 1 # we use this to force the change detection
#info = '"%s" added to list' % (item.name)
#self.report({'INFO'}, info)
return {"FINISHED"}

View File

@ -0,0 +1,236 @@
import bpy
from bpy.props import (StringProperty, BoolProperty, PointerProperty)
from bpy_types import (PropertyGroup)
from ..propGroups.conversions import property_group_value_from_custom_property_value, property_group_value_to_custom_property_value
class ComponentInfos(bpy.types.PropertyGroup):
name : bpy.props.StringProperty(
name = "name",
default = ""
)
long_name : bpy.props.StringProperty(
name = "long name",
default = ""
)
type_name : bpy.props.StringProperty(
name = "Type",
default = ""
)
values: bpy.props.StringProperty(
name = "Value",
default = ""
)
enabled: BoolProperty(
name="enabled",
description="component enabled",
default=True
)
invalid: BoolProperty(
name="invalid",
description="component is invalid, because of missing registration/ other issues",
default=False
)
invalid_details: StringProperty(
name="invalid details",
description="detailed information about why the component is invalid",
default=""
)
visible: BoolProperty( # REALLY dislike doing this for UI control, but ok hack for now
default=True
)
class ComponentsMeta(PropertyGroup):
infos_per_component: StringProperty(
name="infos per component",
description="component"
)
components: bpy.props.CollectionProperty(type = ComponentInfos)
@classmethod
def register(cls):
bpy.types.Object.components_meta = PointerProperty(type=ComponentsMeta)
@classmethod
def unregister(cls):
del bpy.types.Object.components_meta
# We need a collection property of components PER object
def get_component_metadata_by_short_name(object, short_name):
if not "components_meta" in object:
return None
return next(filter(lambda component: component["name"] == short_name, object.components_meta.components), None)
# remove no longer valid metadata from object
def cleanup_invalid_metadata(object):
components_metadata = object.components_meta.components
to_remove = []
for index, component_meta in enumerate(components_metadata):
short_name = component_meta.name
if short_name not in object.keys():
print("component:", short_name, "present in metadata, but not in object")
to_remove.append(index)
for index in to_remove:
components_metadata.remove(index)
# returns a component definition ( an entry in registry's type_infos) with matching short name or None if nothing has been found
def find_component_definition_from_short_name(short_name):
registry = bpy.context.window_manager.components_registry
long_name = registry.short_names_to_long_names.get(short_name, None)
if long_name != None:
return registry.type_infos.get(long_name, None)
return None
# FIXME: feels a bit heavy duty, should only be done
# if the components panel is active ?
def ensure_metadata_for_all_objects():
for object in bpy.data.objects:
add_metadata_to_components_without_metadata(object)
# returns whether an object has custom properties without matching metadata
def do_object_custom_properties_have_missing_metadata(object):
components_metadata = getattr(object, "components_meta", None)
if components_metadata == None:
return True
components_metadata = components_metadata.components
missing_metadata = False
for component_name in dict(object) :
if component_name == "components_meta":
continue
component_meta = next(filter(lambda component: component["name"] == component_name, components_metadata), None)
if component_meta == None:
# current component has no metadata but is there even a compatible type in the registry ?
# if not ignore it
component_definition = find_component_definition_from_short_name(component_name)
if component_definition != None:
missing_metadata = True
break
return missing_metadata
# adds metadata to object only if it is missing
def add_metadata_to_components_without_metadata(object):
registry = bpy.context.window_manager.components_registry
for component_name in dict(object) :
if component_name == "components_meta":
continue
upsert_component_in_object(object, component_name, registry)
# adds a component to an object (including metadata) using the provided component definition & optional value
def add_component_to_object(object, component_definition, value=None):
cleanup_invalid_metadata(object)
if object is not None:
print("add_component_to_object", component_definition)
long_name = component_definition["title"]
short_name = component_definition["short_name"]
registry = bpy.context.window_manager.components_registry
if registry.type_infos == None:
raise Exception('registry type infos have not been loaded yet or ar missing !')
definition = registry.type_infos[long_name]
# now we use our pre_generated property groups to set the initial value of our custom property
(_, propertyGroup) = upsert_component_in_object(object, component_name=short_name, registry=registry)
if value == None:
value = property_group_value_to_custom_property_value(propertyGroup, definition, registry, None)
else: # we have provided a value, that is a raw , custom property value, to set the value of the propertyGroup
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, definition, registry, value)
del object["__disable__update"]
object[short_name] = value
def upsert_component_in_object(object, component_name, registry):
# print("upsert_component_in_object", object, "component name", component_name)
# TODO: upsert this part too ?
target_components_metadata = object.components_meta.components
component_definition = find_component_definition_from_short_name(component_name)
if component_definition != None:
short_name = component_definition["short_name"]
long_name = component_definition["title"]
property_group_name = short_name+"_ui"
propertyGroup = None
component_meta = next(filter(lambda component: component["name"] == short_name, target_components_metadata), None)
if not component_meta:
component_meta = target_components_metadata.add()
component_meta.name = short_name
component_meta.long_name = long_name
propertyGroup = getattr(component_meta, property_group_name, None)
else: # this one has metadata but we check that the relevant property group is present
propertyGroup = getattr(component_meta, property_group_name, None)
# try to inject propertyGroup if not present
if propertyGroup == None:
#print("propertygroup not found in metadata attempting to inject")
if property_group_name in registry.component_propertyGroups:
# we have found a matching property_group, so try to inject it
# now inject property group
setattr(ComponentInfos, property_group_name, registry.component_propertyGroups[property_group_name]) # FIXME: not ideal as all ComponentInfos get the propGroup, but have not found a way to assign it per instance
propertyGroup = getattr(component_meta, property_group_name, None)
# now deal with property groups details
if propertyGroup != None:
if short_name in registry.invalid_components:
component_meta.enabled = False
component_meta.invalid = True
component_meta.invalid_details = "component contains fields that are not in the schema, disabling"
else:
# if we still have not found the property group, mark it as invalid
component_meta.enabled = False
component_meta.invalid = True
component_meta.invalid_details = "component not present in the schema, possibly renamed? Disabling for now"
# property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, object[component_name])
return (component_meta, propertyGroup)
else:
return(None, None)
def copy_propertyGroup_values_to_another_object(source_object, target_object, component_name):
if source_object == None or target_object == None or component_name == None:
raise Exception('missing input data, cannot copy component propertryGroup')
component_definition = find_component_definition_from_short_name(component_name)
short_name = component_definition["short_name"]
property_group_name = short_name+"_ui"
registry = bpy.context.window_manager.components_registry
source_components_metadata = source_object.components_meta.components
source_componentMeta = next(filter(lambda component: component["name"] == short_name, source_components_metadata), None)
# matching component means we already have this type of component
source_propertyGroup = getattr(source_componentMeta, property_group_name)
# now deal with the target object
(_, target_propertyGroup) = upsert_component_in_object(target_object, component_name, registry)
# add to object
value = property_group_value_to_custom_property_value(target_propertyGroup, component_definition, registry, None)
target_object[short_name] = value
# copy the values over
for field_name in source_propertyGroup.field_names:
if field_name in source_propertyGroup:
target_propertyGroup[field_name] = source_propertyGroup[field_name]
apply_propertyGroup_values_to_object_customProperties(target_object)
# TODO: move to propgroups ?
def apply_propertyGroup_values_to_object_customProperties(object):
cleanup_invalid_metadata(object)
registry = bpy.context.window_manager.components_registry
for component_name in dict(object) :
if component_name == "components_meta":
continue
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_short_name(component_name)
if component_definition != None:
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value

View File

@ -0,0 +1,153 @@
import ast
import json
import bpy
from bpy_types import Operator
from bpy.props import (StringProperty)
from .metadata import add_component_to_object, add_metadata_to_components_without_metadata, copy_propertyGroup_values_to_another_object, find_component_definition_from_short_name
class AddComponentOperator(Operator):
"""Add component to blueprint"""
bl_idname = "object.addblueprint_to_component"
bl_label = "Add component to blueprint Operator"
bl_options = {"UNDO"}
component_type: StringProperty(
name="component_type",
description="component type to add",
)
def execute(self, context):
print("adding component to blueprint", self.component_type)
object = context.object
has_component_type = self.component_type != ""
if has_component_type and object != None:
type_infos = context.window_manager.components_registry.type_infos
component_definition = type_infos[self.component_type]
add_component_to_object(object, component_definition)
return {'FINISHED'}
class CopyComponentOperator(Operator):
"""Copy component from blueprint"""
bl_idname = "object.copy_component"
bl_label = "Copy component Operator"
bl_options = {"UNDO"}
source_component_name: StringProperty(
name="source component_name",
description="name of the component to copy",
)
source_object_name: StringProperty(
name="source object name",
description="name of the object to copy the component from",
)
@classmethod
def register(cls):
bpy.types.WindowManager.copied_source_component_name = StringProperty()
bpy.types.WindowManager.copied_source_object = StringProperty()
@classmethod
def unregister(cls):
del bpy.types.WindowManager.copied_source_component_name
del bpy.types.WindowManager.copied_source_object
def execute(self, context):
if self.source_component_name != '' and self.source_object_name != "":
context.window_manager.copied_source_component_name = self.source_component_name
context.window_manager.copied_source_object = self.source_object_name
else:
self.report({"ERROR"}, "The source object name / component name to copy a component from have not been specified")
return {'FINISHED'}
class PasteComponentOperator(Operator):
"""Paste component to blueprint"""
bl_idname = "object.paste_component"
bl_label = "Paste component to blueprint Operator"
bl_options = {"UNDO"}
def execute(self, context):
source_object_name = context.window_manager.copied_source_object
source_object = bpy.data.objects.get(source_object_name, None)
print("source object", source_object)
if source_object == None:
self.report({"ERROR"}, "The source object to copy a component from does not exist")
else:
component_name = context.window_manager.copied_source_component_name
if not component_name in source_object:
self.report({"ERROR"}, "The source component to copy a component from does not exist")
else:
component_value = source_object[component_name]
print("pasting component to object: component name:", str(component_name), "component value:" + str(component_value))
print (context.object)
copy_propertyGroup_values_to_another_object(source_object, context.object, component_name)
return {'FINISHED'}
class DeleteComponentOperator(Operator):
"""Delete component from blueprint"""
bl_idname = "object.delete_component"
bl_label = "Delete component from blueprint Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to delete",
)
def execute(self, context):
object = context.object
if object is not None and self.component_name in object:
del object[self.component_name]
else:
self.report({"ERROR"}, "The object/ component to remove does not exist")
return {'FINISHED'}
class GenerateComponent_From_custom_property_Operator(Operator):
"""generate components from custom property"""
bl_idname = "object.generate_component"
bl_label = "Generate component from custom_property Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to generate custom properties for",
)
def execute(self, context):
object = context.object
add_metadata_to_components_without_metadata(object)
return {'FINISHED'}
class Toggle_ComponentVisibility(Operator):
"""toggles components visibility"""
bl_idname = "object.toggle_component_visibility"
bl_label = "Toggle component visibility"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to toggle",
)
def execute(self, context):
object = context.object
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["name"] == self.component_name, components_in_object), None)
if component_meta != None:
component_meta.visible = not component_meta.visible
return {'FINISHED'}

View File

@ -0,0 +1,212 @@
import json
import bpy
from .metadata import do_object_custom_properties_have_missing_metadata
from .operators import AddComponentOperator, CopyComponentOperator, DeleteComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility
def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum")
is_list = getattr(propertyGroup, "with_list")
#current_short_name = getattr(propertyGroup, "short_name", "") + "_ui"
#nesting = nesting + [current_short_name] # we need this convoluted "nested path strings " workaround so that operators working on a given
# item in our components hierarchy can get the correct propertyGroup by STRINGS because of course, we cannot pass objects to operators...sigh
# if it is an enum, the first field name is always the list of enum variants, the others are the variants
field_names = propertyGroup.field_names
#print("")
#print("drawing", propertyGroup, nesting, "component_name", rootName)
#type_name = getattr(propertyGroup, "type_name", None)#propertyGroup.type_name if "type_name" in propertyGroup else ""
#print("type name", type_name)
#print("name", propertyGroup.name, "name2", getattr(propertyGroup, "name"), "short_name", getattr(propertyGroup, "short_name", None), "nesting", nesting)
if is_enum:
subrow = layout.row()
display_name = field_names[0] if propertyGroup.tupple_or_struct == "struct" else ""
subrow.prop(propertyGroup, field_names[0], text=display_name)
subrow.separator()
selection = getattr(propertyGroup, field_names[0])
for fname in field_names[1:]:
if fname == "variant_" + selection:
subrow = layout.row()
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
#print("nestedPropertyGroup", nestedPropertyGroup, fname, nested)
if nested:
draw_propertyGroup(nestedPropertyGroup, subrow.column(), nesting + [fname], rootName )
# if an enum variant is not a propertyGroup
break
elif is_list:
#print("show list", propertyGroup, dict(propertyGroup), propertyGroup.type_name)
item_list = getattr(propertyGroup, "list")
item_type = getattr(propertyGroup, "type_name_short")
list_index = getattr(propertyGroup, "list_index")
box = layout.box()
split = box.split(factor=0.9)
list_column, buttons_column = (split.column(),split.column())
list_column = list_column.box()
for index, item in enumerate(item_list):
row = list_column.row()
draw_propertyGroup(item, row, nesting, rootName)
icon = 'CHECKBOX_HLT' if list_index == index else 'CHECKBOX_DEHLT'
op = row.operator('generic_list.select_item', icon=icon, text="")
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
op.selection_index = index
#various control buttons
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='ADD', text="")
op.action = 'ADD'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='REMOVE', text="")
op.action = 'REMOVE'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_UP', text="")
op.action = 'UP'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_DOWN', text="")
op.action = 'DOWN'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
else:
for fname in field_names:
subrow = layout.row()
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
if nested:
layout.separator()
layout.label(text=display_name) # this is the name of the field/sub field
layout.separator()
draw_propertyGroup(nestedPropertyGroup, subrow.column(), nesting + [fname], rootName )
else:
subrow.prop(propertyGroup, fname, text=display_name)
subrow.separator()
class BEVY_COMPONENTS_PT_ComponentsPanel(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_ComponentsPanel"
bl_label = "Components"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
@classmethod
def poll(cls, context):
return (context.object is not None)
def draw(self, context):
object = context.object
layout = self.layout
# we get & load our component registry
registry = bpy.context.window_manager.components_registry
available_components = bpy.context.window_manager.components_list
if object is not None:
row = layout.row(align=True)
row.prop(available_components, "list", text="Component")
row.prop(available_components, "filter",text="Filter")
# add components
row = layout.row(align=True)
op = row.operator(AddComponentOperator.bl_idname, text="Add", icon="ADD")
op.component_type = available_components.list
row.enabled = available_components.list != ''
layout.separator()
# paste components
row = layout.row(align=True)
row.operator(PasteComponentOperator.bl_idname, text="Paste component ("+bpy.context.window_manager.copied_source_component_name+")", icon="PASTEDOWN")
row.enabled = registry.type_infos != None and context.window_manager.copied_source_object != ''
layout.separator()
# upgrate custom props to components
upgradeable_customProperties = registry.type_infos != None and do_object_custom_properties_have_missing_metadata(context.object)
if upgradeable_customProperties:
row = layout.row(align=True)
op = row.operator(GenerateComponent_From_custom_property_Operator.bl_idname, text="generate components from custom properties" , icon="LOOP_FORWARDS")
layout.separator()
components_in_object = object.components_meta.components
for component_name in sorted(dict(object)) : # sorted by component name, practical
if component_name == "components_meta":
continue
# anything withouth metadata gets skipped, we only want to see real components, not all custom props
component_meta = next(filter(lambda component: component["name"] == component_name, components_in_object), None)
if component_meta == None:
continue
component_invalid = getattr(component_meta, "invalid")
invalid_details = getattr(component_meta, "invalid_details")
component_visible = getattr(component_meta, "visible")
single_field = False
# our whole row
box = layout.box()
row = box.row(align=True)
# "header"
row.alert = component_invalid
row.prop(component_meta, "enabled", text="")
row.label(text=component_name)
# we fetch the matching ui property group
root_propertyGroup_name = component_name+"_ui"
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
# if the component has only 0 or 1 field names, display inline, otherwise change layout
single_field = len(propertyGroup.field_names) < 2
prop_group_location = box.row(align=True).column()
if single_field:
prop_group_location = row.column(align=True)#.split(factor=0.9)#layout.row(align=False)
if component_visible:
if component_invalid:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
prop_group_location.label(text=error_message)
draw_propertyGroup(propertyGroup, prop_group_location, [root_propertyGroup_name], component_name)
else :
row.label(text="details hidden, click on toggle to display")
else:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
row.label(text=error_message)
# "footer" with additional controls
op = row.operator(DeleteComponentOperator.bl_idname, text="", icon="X")
op.component_name = component_name
row.separator()
op = row.operator(CopyComponentOperator.bl_idname, text="", icon="COPYDOWN")
op.source_component_name = component_name
op.source_object_name = object.name
row.separator()
#if not single_field:
toggle_icon = "TRIA_DOWN" if component_visible else "TRIA_RIGHT"
op = row.operator(Toggle_ComponentVisibility.bl_idname, text="", icon=toggle_icon)
op.component_name = component_name
#row.separator()
else:
layout.label(text ="Select an object to edit its components")

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,31 @@
import bpy
import json
# Makes an empty, at the specified location, rotation, scale stores it in existing collection, from https://blender.stackexchange.com/questions/51290/how-to-add-empty-object-not-using-bpy-ops
def make_empty(name, location, rotation, scale, collection):
object_data = None
empty_obj = bpy.data.objects.new( name, object_data )
empty_obj.empty_display_size = 2
empty_obj.empty_display_type = 'PLAIN_AXES'
empty_obj.name = name
empty_obj.location = location
empty_obj.scale = scale
empty_obj.rotation_euler = rotation
collection.objects.link( empty_obj )
#bpy.context.view_layer.update()
return empty_obj
#".gltf_auto_export_settings"
def upsert_settings(name, data):
stored_settings = bpy.data.texts[name] if name in bpy.data.texts else bpy.data.texts.new(name)
stored_settings.clear()
stored_settings.write(json.dumps(data))
def load_settings(name):
stored_settings = bpy.data.texts[name] if name in bpy.data.texts else None
if stored_settings != None:
return json.loads(stored_settings.as_string())
return None

View File

@ -0,0 +1,221 @@
import json
from bpy_types import PropertyGroup
conversion_tables = {
"bool": lambda value: value,
"char": lambda value: '"'+value+'"',
"str": lambda value: '"'+value+'"',
"alloc::string::String": lambda value: '"'+value+'"',
"glam::Vec2": lambda value: "Vec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::DVec2": lambda value: "DVec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::UVec2": lambda value: "UVec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::Vec3": lambda value: "Vec3(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::Vec3A": lambda value: "Vec3A(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::UVec3": lambda value: "UVec3(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::Vec4": lambda value: "Vec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::DVec4": lambda value: "DVec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::UVec4": lambda value: "UVec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::Quat": lambda value: "Quat(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"bevy_render::color::Color": lambda value: "Rgba(red:"+str(value[0])+ ", green:"+str(value[1])+ ", blue:"+str(value[2])+ ", alpha:"+str(value[3])+ ")",
}
#converts the value of a property group(no matter its complexity) into a single custom property value
# this is more or less a glorified "to_ron()" method (not quite but close to)
def property_group_value_to_custom_property_value(property_group, definition, registry, parent=None, value=None):
component_name = definition["short_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
type_name = definition["title"]
is_value_type = type_name in conversion_tables
#print("computing custom property", component_name, type_info, type_def, type_name)
if is_value_type:
value = conversion_tables[type_name](value)
elif type_info == "Struct":
values = {}
if len(property_group.field_names) ==0:
value = ''
else:
for index, field_name in enumerate(property_group.field_names):
item_type_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_type_name] if item_type_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=component_name, value=value)
else:
value = '""'
values[field_name] = value
value = values
elif type_info == "Tuple":
values = {}
for index, field_name in enumerate(property_group.field_names):
item_type_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_type_name] if item_type_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=component_name, value=value)
else:
value = '""'
values[field_name] = value
value = tuple(e for e in list(values.values()))
elif type_info == "TupleStruct":
values = {}
for index, field_name in enumerate(property_group.field_names):
item_type_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_type_name] if item_type_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=component_name, value=value)
else:
value = '""'
values[field_name] = value
value = tuple(e for e in list(values.values()))
elif type_info == "Enum":
selected = getattr(property_group, component_name)
if type_def == "object":
selection_index = property_group.field_names.index("variant_"+selected)
variant_name = property_group.field_names[selection_index]
variant_definition = definition["oneOf"][selection_index-1]
if "prefixItems" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=component_name, value=value)
value = selected + str(value,)
elif "properties" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=component_name, value=value)
value = selected + str(value,)
else:
print("basic enum stuff")
value = selected # here the value of the enum is just the name of the variant
else:
value = selected
elif type_info == "List":
item_list = getattr(property_group, "list")
#item_type = getattr(property_group, "type_name_short")
value = []
for item in item_list:
item_type_name = getattr(item, "type_name")
definition = registry.type_infos[item_type_name] if item_type_name in registry.type_infos else None
if definition != None:
item_value = property_group_value_to_custom_property_value(item, definition, registry, component_name, None)
if item_type_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
item_value = item_value[0]
else:
item_value = '""'
value.append(item_value)
else:
value = conversion_tables[type_name](value) if is_value_type else value
#print("VALUE", value, type(value))
#print("generating custom property value", value, type(value))
if parent == None:
value = str(value).replace("'", "")
value = value.replace(",)",")")
value = value.replace("{", "(").replace("}", ")")
value = value.replace("True", "true").replace("False", "false")
return value
import re
#converts the value of a single custom property into a value (values) of a property group
def property_group_value_from_custom_property_value(property_group, definition, registry, custom_property_value):
#print(" ")
#print("setting property group value", property_group, definition, custom_property_value)
type_infos = registry.type_infos
value_types_defaults = registry.value_types_defaults
print("custom_property_value", custom_property_value)
def parse_field(item, property_group, definition, field_name):
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
properties = definition["properties"] if "properties" in definition else {}
prefixItems = definition["prefixItems"] if "prefixItems" in definition else []
has_properties = len(properties.keys()) > 0
has_prefixItems = len(prefixItems) > 0
is_enum = type_info == "Enum"
is_list = type_info == "List"
is_value_type = type_def in value_types_defaults
print("parsing field", item, "type infos", type_info, "type_def", type_def)
if type_info == "Struct":
print("is object")
for field_name in property_group.field_names:
print("field name", field_name)
# sub field
if isinstance(getattr(property_group, field_name), PropertyGroup):
sub_prop_group = getattr(property_group, field_name)
ref_name = properties[field_name]["type"]["$ref"].replace("#/$defs/", "")
sub_definition = type_infos[ref_name]
parse_field(item[field_name], sub_prop_group, sub_definition, field_name)
else:
setattr(property_group, field_name, item[field_name])
if has_prefixItems:
if len(property_group.field_names) == 1:
setattr(property_group, "0", item) # FIXME: not ideal
else:
for field_name in property_group.field_names:
setattr(property_group, field_name, item)
if is_enum:
if type_def == "object":
regexp = re.search('(^[^\(]+)(\((.*)\))', item)
chosen_variant = regexp.group(1)
chosen_variant_value = regexp.group(3).replace("'", '"').replace("(", "[").replace(")","]")
chosen_variant_value = json.loads(chosen_variant_value)
# first set chosen selection
field_name = property_group.field_names[0]
setattr(property_group, field_name, chosen_variant)
# thenlook for the information about the matching variant
sub_definition= None
for variant in definition["oneOf"]:
if variant["title"] == chosen_variant:
ref_name = variant["prefixItems"][0]["type"]["$ref"].replace("#/$defs/", "")
sub_definition = type_infos[ref_name]
break
variant_name = "variant_"+chosen_variant
if isinstance(getattr(property_group, variant_name), PropertyGroup):
sub_prop_group = getattr(property_group, variant_name)
parse_field(chosen_variant_value, sub_prop_group, sub_definition, variant_name)
else:
setattr(property_group, variant_name, chosen_variant_value)
else:
field_name = property_group.field_names[0]
setattr(property_group, field_name, item)
if is_list:
print("is list")
if is_value_type:
print("is value type")
try:
parse_field(custom_property_value, property_group, definition, None)
except Exception as error:
print("failed to parse raw custom property data", error)

View File

@ -0,0 +1,95 @@
import bpy
from bpy_types import PropertyGroup
from bpy.props import (PointerProperty)
from . import process_structs
from . import process_tupples
from . import process_enum
from . import process_list
def process_component(registry, definition, update, extras=None, nesting = []):
component_name = definition['title']
short_name = definition["short_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
properties = definition["properties"] if "properties" in definition else {}
prefixItems = definition["prefixItems"] if "prefixItems" in definition else []
has_properties = len(properties.keys()) > 0
has_prefixItems = len(prefixItems) > 0
is_enum = type_info == "Enum"
is_list = type_info == "List"
# print("processing", short_name, component_name, type_def, type_info)
__annotations__ = {}
tupple_or_struct = None
with_properties = False
with_items = False
with_enum = False
with_list = False
if has_properties:
__annotations__ = __annotations__ | process_structs.process_structs(registry, definition, properties, update, nesting)
with_properties = True
tupple_or_struct = "struct"
if has_prefixItems:
__annotations__ = __annotations__ | process_tupples.process_tupples(registry, definition, prefixItems, update, nesting)
with_items = True
tupple_or_struct = "tupple"
if is_enum:
__annotations__ = __annotations__ | process_enum.process_enum(registry, definition, update, nesting)
with_enum = True
if is_list:
__annotations__ = __annotations__ | process_list.process_list(registry, definition, update, nesting)
with_list= True
field_names = []
for a in __annotations__:
field_names.append(a)
extras = extras if extras is not None else {
"type_name": component_name
}
root_component = nesting[0] if len(nesting) > 0 else component_name
# print("DONE:",short_name,"__annotations__", __annotations__)
# print("")
# property_group_name = short_name+"_ui"
property_group_params = {
**extras,
'__annotations__': __annotations__,
'tupple_or_struct': tupple_or_struct,
'field_names': field_names,
**dict(with_properties = with_properties, with_items= with_items, with_enum= with_enum, with_list= with_list, short_name= short_name),
'root_component': root_component
}
#FIXME: YIKES, but have not found another way:
""" Withouth this ; the following does not work
-BasicTest
- NestingTestLevel2
-BasicTest => the registration & update callback of this one overwrites the first "basicTest"
have not found a cleaner workaround so far
"""
property_group_name = str(hash(str(nesting))) + short_name+"_ui" if len(nesting) > 0 else short_name+"_ui"
(property_group_pointer, property_group_class) = property_group_from_infos(property_group_name, property_group_params)
# add our component propertyGroup to the registry
registry.register_component_propertyGroup(property_group_name, property_group_pointer)
# for practicality, we add an entry for a reverse lookup (short => long name, since we already have long_name => short_name with the keys of the raw registry)
registry.add_shortName_to_longName(short_name, component_name)
return (property_group_pointer, property_group_class)
def property_group_from_infos(property_group_name, property_group_parameters):
# print("creating property group", property_group_name)
property_group_class = type(property_group_name, (PropertyGroup,), property_group_parameters)
bpy.utils.register_class(property_group_class)
property_group_pointer = PointerProperty(type=property_group_class)
return (property_group_pointer, property_group_class)

Some files were not shown because too many files have changed in this diff Show More