refactor(Blenvy): BIG cleanup

* removed old blender add-ons, there is now only Blenvy !
 * updated most of the main docs accordingly
 * updated project name in main README
 * etc
 * related tweaks
This commit is contained in:
kaosat.dev 2024-06-10 14:08:16 +02:00
parent 763a520b32
commit 295c387132
153 changed files with 19 additions and 12737 deletions

View File

@ -8,7 +8,7 @@ The workflow goes as follows (once you got your Bevy code setup)
## Component creation
Setup the Blender [bevy_components](./tools/bevy_components/README.md) add-on
Setup the Blender [bevy_components](./tools/blenvy/README.md) add-on
to add & edit your components visually & reliably
![bevy_components](./docs/bevy_components.png)
@ -22,7 +22,7 @@ to add & edit your components visually & reliably
- 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/)
- or much better, using [blenvy](./tools/blenvy/)
## Now use your gltf files in Bevy

View File

@ -2,7 +2,7 @@
[![License](https://img.shields.io/crates/l/bevy_gltf_components)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/LICENSE.md)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F5TO32O)
# Blender_bevy_components_workflow
# Blenvy: a Blender <=> bevy components workflow
![demo](./docs/blender_bevy.png)
@ -18,8 +18,7 @@ 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
* 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 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)
* now also with an UI tool to add & edit Bevy components, automatically export gltf blueprints & more in [Blender](./tools/blenvy/README.md)
* 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
* minimal setup & code, you can have something basic running fast
@ -45,23 +44,19 @@ bevy_gltf_blueprints to only save a minimal subset of dynamic data, seperating d
The examples for the crate are [here](./examples/bevy_gltf_save_load/)
> 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
- [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 [blenvy](./tools/blenvy/README.md) add-on
## Tools
### Blender: gltf_auto_export
### Blender: blenvy
- for convenience I also added a [Blender addon](./tools/gltf_auto_export/README.md) that automatically exports your level/world from Blender to gltf whenever you save your Blend file
- it also supports automatical exports of collections as [Gltf blueprints](./crates/bevy_gltf_blueprints/README.md) & more !
- an all in one [Blender addon](./tools/blenvy/README.md) for the Blender side of the workflow:
- allow easilly adding & editing Bevy components , using automatically generated UIs for each component
- automatically exports your level/world from Blender to gltf whenever you save your Blend file
- it also supports automatical exports of collections as [Gltf blueprints](./crates/bevy_gltf_blueprints/README.md) &
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
Please read the [README]((./tools/blenvy/README.md)) of the add-on for installation & use instructions

View File

@ -15,9 +15,8 @@ A blueprint is a set of **overrideable** components + a hierarchy: ie
* just a Gltf file with Gltf_extras specifying components
* a component called BlueprintName
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-ons that do a lot of the work for you
- [gltf_auto_export](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
- [bevy_components](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components)
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-on that do a lot of the work for you
- [blenvy](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy)
## Usage

View File

@ -50,7 +50,6 @@ impl Default for BluePrintBundle {
#[derive(Clone, Resource)]
pub struct BluePrintsConfig {
pub(crate) format: GltfFormat,
pub(crate) aabbs: bool,
pub(crate) aabb_cache: HashMap<String, Aabb>, // cache for aabbs
@ -81,7 +80,6 @@ impl fmt::Display for GltfFormat {
#[derive(Debug, Clone)]
/// Plugin for gltf blueprints
pub struct BlueprintsPlugin {
pub format: GltfFormat,
/// Automatically generate aabbs for the blueprints root objects
pub aabbs: bool,
///
@ -91,7 +89,6 @@ pub struct BlueprintsPlugin {
impl Default for BlueprintsPlugin {
fn default() -> Self {
Self {
format: GltfFormat::GLB,
aabbs: false,
material_library: false
}
@ -131,7 +128,6 @@ impl Plugin for BlueprintsPlugin {
.register_type::<HashMap<String, Vec<String>>>()
.insert_resource(BluePrintsConfig {
format: self.format,
aabbs: self.aabbs,
aabb_cache: HashMap::new(),

View File

@ -6,7 +6,7 @@
# 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
Its main use case is as a backbone for the [```blenvy``` Blender add-on](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy), 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).

View File

@ -5,7 +5,6 @@ pub struct CorePlugin;
impl Plugin for CorePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((BlueprintsPlugin {
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},));

View File

@ -4,7 +4,7 @@ This example showcases how to use ```bevy_save_load``` crate to save & load your
## Notes Workflow with blender / demo information
- the gltf files for this demo where generated using the **Export dynamic and static objects seperatly** feature of the auto_export addon:
- the gltf files for this demo where generated using the **Export dynamic and static objects seperatly** feature of the blenvy addon:
so the static & dynamic level files where generated automatically, based on the entities that have a **Dynamic** component/custom property

View File

@ -2,7 +2,7 @@
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,
* That file is then used by the [Blender addon](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy) to create Uis for each component,
to be able to add & edit Bevy components easilly in Blender !

View File

@ -20,8 +20,6 @@ impl Plugin for CorePlugin {
..Default::default()
},
BlueprintsPlugin {
library_folder: "blueprints".into(),
format: GltfFormat::GLB,
material_library: true,
aabbs: true,
..Default::default()

View File

@ -1,297 +0,0 @@
# 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!.
> IMPORTANT !! if you have previously used v0.1 , v0.2 had a breaking change, please see [this](#regenerate-ui-values) section on how to upgrade your data to v0.2.\
This problem should not be present going forward
> IMPORTANT !! if you have previously used v0.2 , v0.3 had a breaking change, please see [this](#regenerate-custom-property-values) section on how to upgrade your data to v0.3.
## 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)
#### registry file polling
* by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly
* you can set the polling frequency or turn it off if you do not want auto-refresh
![registry file polling](./docs/registry_polling.png)
## Use
### 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)
### Create components from custom properties
- IF you have a valid component type and the correct corresponding RON string in the custom_property value (this button will not appear if not), this add-on can automatically
generate the corresponding component for you:
- Fill/check your custom property (here for Aabb)
![generate_components 2](./docs/generate_components2.png)
- click on the button
![generate_components](./docs/generate_components.png)
-voila !
![generate_components 3](./docs/generate_components3.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)
> see [here](#invalidunregistered-type-renaming--conversion) for ways to convert invalid / unregistered components to other types.
- 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 Tools
In this section you will find various additional more advanced tooling
### Invalid/unregistered type renaming / conversion
If you have components that are
* invalid : ie some error was diagnosed
* unregistered: a custom property is present on the object, but there is no matching type in the registry
Here you will get an overview, of ALL invalid and unregistered components in your Blender project, so you can find them, rename/convert them,
or delete them, also in bulk
![component rename overview](./docs/component_rename_overview2.png)
* you can click on the button to select the object in your outliner (this also works across scenes, so you will be taken to the scene where the
given object is located)
![update custom properties](./docs/component_rename_object_select.png)
#### Single object component renaming/ conversion
- to rename/convert a single component for a single object:
* go to the row of the object you want to convert the component of
* in the dropdown menu, choose the target component
* click on the button with the magic wand to convert the component
![single rename](./docs/component_rename_single.png)
> the tool will attempt to automatically convert the source component, including the field names/values, if the target component has the same ones
If it fails to do the conversion, you will get an error message, and you will either have to change the custom property yourself, or you can simply
change the values in the UI, which will automatically generate the custom property value
- to delete a single component for a single object:
* go to the row of the object you want to remove the component from
* click on the button with the "x" to remove the component
![single delete](./docs/component_remove_single.png)
#### Bulk component renaming/ conversion
- use this method if you want to convert ALL components of a given type of ALL objects
* click on this button to pick your source component
![bulk convert remove](./docs/component_rename_remove_bulk.png)
* for conversion: in the dropdown menu, choose the target component & click apply to convert all matching components
* for deletion: clic on the "x" to remove all matching components
![bulk convert remove](./docs/component_rename_remove_bulk2.png)
### For conversion between custom properties & components & vice-versa
#### regenerate custom property values
- "update custom properties of current object" : will go over **all components** that you have defined for the **currently selected object**, and re-generate the
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)
> IMPORTANT !! use this if you have previously used v0.1 or v0.2 , as v0.3 had a breaking change, that makes it **necessary** to use this **once** to upgrade components data
You should also re-export your gltf files , otherwise you might run into issues
#### regenerate component/ UI values
- since v0.2, you have the option to regenerate (for the selected object or all objects, as above) to regenerate your UI values from the custom property values
![update UI FROM custom properties](./docs/update_ui_from_custom_properties.png)
> IMPORTANT !! use this if you have previously used v0.1 , as v0.2 had a breaking change, that makes it **necessary** to use this **once** to upgrade the UI data
> Note: the legacy mode support has been removed since version
## Examples
you can find an example [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/)
## Known issues & limitations:
* **Range** data (ie ```Range<f32>``` etc) are not handled at this time (issue seems to be on the Bevy side)
* **Entity** structs are always set to 0 (setting entity values on the Blender side at this time does not make much sense anyway)
## 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

@ -1,229 +0,0 @@
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 {
"long_name": "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 long_name
- [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
- [x] add support for "enabled" flag
- [ ] add special components
- "AutoExport" => Needed
- "Dynamic" ? naah wait that should be exported by the Bevy side
- [x] 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 ?
- [x] release all versions
- [x] update main documentation, add compatibility version grid
## Phase 2
- [x] fix handling of long component names
- [x] fix nesting level handling issue for new system : ie basic component DOES NOT work, but nestedLevel2 does
- add goddam tests !
- [ ] verify some weird prop => custom property values (Calculated Clip for example)
- [x] fix "reload registry" not clearing all previous data (reloading registry does not seem to account for added/removed components in the registry )
- add file watcher for registry
- [x] have the watcher work as expected
- [ ] add handling of removed registry file
- [ ] clear & reset handler when the file browser for the registry is used
- [ ] re-enable watcher
- tests
clear && pytest -svv --blender-executable <path_to_blender>/blender/blender-4.0.2-linux-x64/blender
- [x] load registry
- just check list of components vs lists in registry
- [x] try adding all components
- [x] select an object
- [x] call the add_component operator
- [x] change params
- use field names + component definitions to set values
- [x] find a way to shuffle params of ALL components based on a reliable, repeatable seed
- [x] test propgroup values => custom property values
- [x] test custom property value => propgroup value
- check if all went well
- [x] fix issues with incorect custom_property generation
- [x] fix issue with object variants for enums
- [ ] add handling for core::ops::Range<f32> & other ranges
- [x] add handling for alloc::borrow::Cow<str>
- [x] add handling of isize
- [x] indirection level
- currently
- short_name +_"ui => direct lookup
- problem : max 64 chars for propertyGroupNames
- possible solution
- propertyGroupName storage: simple , incremented INT (call it propGroupId for ex)
- lookup shortName => propGroupId
- do a first pass, by replacing manual propGroupNames creation with a function
- in a second pass, replace the innards
- add button to regenerate cutom prop values from custom properties (allows us to sidestep any future issues with internals changing)
- [x] fix lists
- [x] fix enums (see Clusterconfig)
- [x] need an example with one tupple one struct
- [x] projection
- [x] additionalmassproperties
- [x] fix tupleStructs (see TupleVecF32F32) => always the same problem of having us pre-parse data without knowing what we have inside
- find a way to only split by level 0 (highest level) nesting "," seperators, ignoring any level of nesting until we dig one level deeper
- solve nesting level use issues
- [x] remove metadata when deleting components
- [x] add try catch around custom_prop => propGroup
- [x] enhance the GenerateComponent_From_custom_property_Operator to use the new system to actually generate the stuff
- coherence in operators:
- component_name vs component_type
- [x] delete => remove
- [x] clean up reloading of registry settings
- [x] clean up file watcher
=========================================
Restructuring of storage of components
- [x] marking of invalid root propgroups/components should be based on long name
- [x] overhaul & check each prop group type's use of short names => long names
- [x] lists
- [x] property_name = short_name in process enum: will likely require to use another indirection helper to keep the propery names short
- [x] in conversions from propgroups
component_name = definition["short_name"]
- [ ] fix is_component_valid that is used in gltf_auto_export
- [x] update all tests
- Hashmap Support
- [x] fix parsing of keys's type either on Bevy side (prefered) or on the Blender side
- [x] fix weird issue with missing "0" property when adding new entry in empty hashmap => happens only if the values for the "setter" have never been set
- [ ] handle missing types in registry for keys & values
- [ ] Add correct upgrade handling from individual component to bevy_components

View File

@ -1,138 +0,0 @@
bl_info = {
"name": "bevy_components",
"author": "kaosigh",
"version": (0, 4, 1),
"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, Fix_Component_Operator, OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, RenameHelper, Toggle_ComponentVisibility
from .registry.registry import ComponentsRegistry,MissingBevyType
from .registry.operators import (COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, OT_select_component_name_to_replace, OT_select_object, ReloadRegistryOperator, OT_OpenFilebrowser)
from .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_AdvancedToolsPanel, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List)
from .components.metadata import (ComponentMetadata, ComponentsMeta)
from .components.lists import GENERIC_LIST_OT_actions, Generic_LIST_OT_AddItem, Generic_LIST_OT_RemoveItem, Generic_LIST_OT_SelectItem
from .components.maps import GENERIC_MAP_OT_actions
from .components.definitions_list import (ComponentDefinitionsList, ClearComponentDefinitionsList)
from .components.ui import (BEVY_COMPONENTS_PT_ComponentsPanel)
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,
RemoveComponentOperator,
RemoveComponentFromAllObjectsOperator,
Fix_Component_Operator,
OT_rename_component,
RenameHelper,
GenerateComponent_From_custom_property_Operator,
Toggle_ComponentVisibility,
ComponentDefinitionsList,
ClearComponentDefinitionsList,
ComponentMetadata,
ComponentsMeta,
MissingBevyType,
ComponentsRegistry,
OT_OpenFilebrowser,
ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_select_object,
OT_select_component_name_to_replace,
BEVY_COMPONENTS_PT_MainPanel,
BEVY_COMPONENTS_PT_ComponentsPanel,
BEVY_COMPONENTS_PT_AdvancedToolsPanel,
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,
GENERIC_MAP_OT_actions
]
from bpy.app.handlers import persistent
@persistent
def post_load(file_name):
registry = bpy.context.window_manager.components_registry
if registry != None:
registry.load_settings()
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

@ -1,41 +0,0 @@
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

@ -1,57 +0,0 @@
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
for long_name in type_infos.keys():
definition = type_infos[long_name]
short_name = definition["short_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))
items.sort(key=lambda a: a[1])
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,
) # type: ignore
filter: StringProperty(
name="component filter",
description="filter for the components list",
options={'TEXTEDIT_UPDATE'}
) # type: ignore
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

@ -1,6 +0,0 @@
import rna_prop_ui
# fake way to make our operator's changes be visible to the change/depsgraph update handler in gltf_auto_export
def ping_depsgraph_update(object):
rna_prop_ui.rna_idprop_ui_create(object, "________temp", default=0)
rna_prop_ui.rna_idprop_ui_prop_clear(object, "________temp")

View File

@ -1,170 +0,0 @@
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="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
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["long_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="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
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["long_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="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
selection_index: IntProperty() # type: ignore
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["long_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", ""))) # type: ignore
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
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["long_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

@ -1,121 +0,0 @@
import json
from bpy_types import Operator, UIList
from bpy.props import (StringProperty, EnumProperty, PointerProperty, FloatVectorProperty, IntProperty)
from ..propGroups.conversions_from_prop_group import property_group_value_to_custom_property_value
class GENERIC_MAP_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "generic_map.map_action"
bl_label = "Map 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", ""))) # type: ignore
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
target_index: IntProperty(name="target index", description="index of item to manipulate")# type: ignore
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["long_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)
keys_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
values_list = getattr(propertyGroup, "values_list")
values_index = getattr(propertyGroup, "values_list_index")
key_setter = getattr(propertyGroup, "keys_setter")
value_setter = getattr(propertyGroup, "values_setter")
if self.action == 'DOWN' and index < len(keys_list) - 1:
#item_next = scn.rule_list[index + 1].name
keys_list.move(index, index + 1)
propertyGroup.list_index += 1
elif self.action == 'UP' and index >= 1:
#item_prev = scn.rule_list[index - 1].name
keys_list.move(index, index - 1)
propertyGroup.list_index -= 1
elif self.action == 'REMOVE':
index = self.target_index
keys_list.remove(index)
values_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(keys_list) - 1)
propertyGroup.values_index = min(max(0, index - 1), len(keys_list) - 1)
if self.action == 'ADD':
print("keys_list", keys_list)
# first we gather all key/value pairs
hashmap = {}
for index, key in enumerate(keys_list):
key_entry = {}
for field_name in key.field_names:
key_entry[field_name] = getattr(key, field_name, None)
value_entry = {}
for field_name in values_list[index].field_names:
value_entry[field_name] = values_list[index][field_name]
hashmap[json.dumps(key_entry)] = index
print("hashmap", hashmap )
# then we need to find the index of a specific value if it exists
key_entry = {}
for field_name in key_setter.field_names:
key_entry[field_name] = getattr(key_setter, field_name, None)
key_to_add = json.dumps(key_entry)
existing_index = hashmap.get(key_to_add, None)
print("existing_index", existing_index)
if existing_index is None:
print("adding new value")
key = keys_list.add()
# copy the values over
for field_name in key_setter.field_names:
val = getattr(key_setter, field_name, None)
if val is not None:
key[field_name] = val
# TODO: add error handling
value = values_list.add()
# copy the values over
for field_name in value_setter.field_names:
val = getattr(value_setter, field_name, None)
if val is not None:
value[field_name] = val
# TODO: add error handling
propertyGroup.list_index = index + 1 # we use this to force the change detection
propertyGroup.values_index = index + 1 # we use this to force the change detection
else:
print("overriding value")
for field_name in value_setter.field_names:
values_list[existing_index][field_name] = value_setter[field_name]
#info = '"%s" added to list' % (item.name)
#self.report({'INFO'}, info)
return {"FINISHED"}

View File

@ -1,344 +0,0 @@
import bpy
from bpy.props import (StringProperty, BoolProperty, PointerProperty)
from bpy_types import (PropertyGroup)
from ..propGroups.conversions_from_prop_group import property_group_value_to_custom_property_value
from ..propGroups.conversions_to_prop_group import property_group_value_from_custom_property_value
class ComponentMetadata(bpy.types.PropertyGroup):
short_name : bpy.props.StringProperty(
name = "name",
default = ""
) # type: ignore
long_name : bpy.props.StringProperty(
name = "long name",
default = ""
) # type: ignore
values: bpy.props.StringProperty(
name = "Value",
default = ""
) # type: ignore
enabled: BoolProperty(
name="enabled",
description="component enabled",
default=True
) # type: ignore
invalid: BoolProperty(
name="invalid",
description="component is invalid, because of missing registration/ other issues",
default=False
) # type: ignore
invalid_details: StringProperty(
name="invalid details",
description="detailed information about why the component is invalid",
default=""
) # type: ignore
visible: BoolProperty( # REALLY dislike doing this for UI control, but ok hack for now
default=True
) # type: ignore
class ComponentsMeta(PropertyGroup):
infos_per_component: StringProperty(
name="infos per component",
description="component"
) # type: ignore
components: bpy.props.CollectionProperty(type = ComponentMetadata) # type: ignore
@classmethod
def register(cls):
bpy.types.Object.components_meta = PointerProperty(type=ComponentsMeta)
@classmethod
def unregister(cls):
del bpy.types.Object.components_meta
# remove no longer valid metadata from object
def cleanup_invalid_metadata(object):
bevy_components = get_bevy_components(object)
if len(bevy_components.keys()) == 0: # no components, bail out
return
components_metadata = object.components_meta.components
to_remove = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if long_name not in bevy_components.keys():
print("component:", long_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 long name or None if nothing has been found
def find_component_definition_from_long_name(long_name):
registry = bpy.context.window_manager.components_registry
return registry.type_infos.get(long_name, 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 get_bevy_components(object) :
if component_name == "components_meta":
continue
component_meta = next(filter(lambda component: component["long_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_long_name(component_name)
if component_definition != None:
missing_metadata = True
break
return missing_metadata
import json
def upsert_bevy_component(object, long_name, value):
if not 'bevy_components' in object:
object['bevy_components'] = '{}'
bevy_components = json.loads(object['bevy_components'])
bevy_components[long_name] = value
object['bevy_components'] = json.dumps(bevy_components)
#object['bevy_components'][long_name] = value # Sigh, this does not work, hits Blender's 63 char length limit
def remove_bevy_component(object, long_name):
if 'bevy_components' in object:
bevy_components = json.loads(object['bevy_components'])
if long_name in bevy_components:
del bevy_components[long_name]
object['bevy_components'] = json.dumps(bevy_components)
if long_name in object:
del object[long_name]
def get_bevy_components(object):
if 'bevy_components' in object:
bevy_components = json.loads(object['bevy_components'])
return bevy_components
return {}
def get_bevy_component_value_by_long_name(object, long_name):
bevy_components = get_bevy_components(object)
if len(bevy_components.keys()) == 0 :
return None
return bevy_components.get(long_name, None)
def is_bevy_component_in_object(object, long_name):
return get_bevy_component_value_by_long_name(object, long_name) is not None
# 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 get_bevy_components(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["long_name"]
registry = bpy.context.window_manager.components_registry
if not registry.has_type_infos():
raise Exception('registry type infos have not been loaded yet or are 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, long_name=long_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"]
upsert_bevy_component(object, long_name, value)
def upsert_component_in_object(object, long_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 = registry.type_infos.get(long_name, None)
if component_definition != None:
short_name = component_definition["short_name"]
long_name = component_definition["long_name"]
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
propertyGroup = None
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
if not component_meta:
component_meta = target_components_metadata.add()
component_meta.short_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(ComponentMetadata, property_group_name, registry.component_propertyGroups[property_group_name]) # FIXME: not ideal as all ComponentMetadata 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 long_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, registry):
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_long_name(component_name)
long_name = component_name
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
registry = bpy.context.window_manager.components_registry
source_components_metadata = source_object.components_meta.components
source_componentMeta = next(filter(lambda component: component["long_name"] == long_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)
upsert_bevy_component(target_object, long_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 get_bevy_components(object) :
"""if component_name == "components_meta":
continue"""
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
upsert_bevy_component(object=object, long_name=component_name, value=value)
# apply component value(s) to custom property of a single component
def apply_propertyGroup_values_to_object_customProperties_for_component(object, component_name):
registry = bpy.context.window_manager.components_registry
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value
components_metadata = object.components_meta.components
componentMeta = next(filter(lambda component: component["long_name"] == component_name, components_metadata), None)
if componentMeta:
componentMeta.invalid = False
componentMeta.invalid_details = ""
def apply_customProperty_values_to_object_propertyGroups(object):
print("apply custom properties to ", object.name)
registry = bpy.context.window_manager.components_registry
for component_name in get_bevy_components(object) :
if component_name == "components_meta":
continue
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
property_group_name = registry.get_propertyGroupName_from_longName(component_name)
components_metadata = object.components_meta.components
source_componentMeta = next(filter(lambda component: component["long_name"] == component_name, components_metadata), None)
# matching component means we already have this type of component
propertyGroup = getattr(source_componentMeta, property_group_name, None)
customProperty_value = get_bevy_component_value_by_long_name(object, component_name)
#value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object["__disable__update"] = True # disable update callback while we set the values of the propertyGroup "tree" (as a propertyGroup can contain other propertyGroups)
property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, customProperty_value)
del object["__disable__update"]
source_componentMeta.invalid = False
source_componentMeta.invalid_details = ""
# removes the given component from the object: removes both the custom property and the matching metadata from the object
def remove_component_from_object(object, component_name):
# remove the component value
remove_bevy_component(object, component_name)
# now remove the component's metadata
components_metadata = getattr(object, "components_meta", None)
if components_metadata == None:
return False
components_metadata = components_metadata.components
to_remove = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if long_name == component_name:
to_remove.append(index)
break
for index in to_remove:
components_metadata.remove(index)
return True
def add_component_from_custom_property(object):
add_metadata_to_components_without_metadata(object)
apply_customProperty_values_to_object_propertyGroups(object)
def rename_component(object, original_long_name, new_long_name):
registry = bpy.context.window_manager.components_registry
type_infos = registry.type_infos
component_definition = type_infos[new_long_name]
component_ron_value = get_bevy_component_value_by_long_name(object=object, long_name=original_long_name)
if component_ron_value is None and original_long_name in object:
component_ron_value = object[original_long_name]
remove_component_from_object(object, original_long_name)
add_component_to_object(object, component_definition, component_ron_value)
def toggle_component(object, component_name):
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_in_object), None)
if component_meta != None:
component_meta.visible = not component_meta.visible

View File

@ -1,321 +0,0 @@
import ast
import json
import bpy
from bpy_types import Operator
from bpy.props import (StringProperty)
from .metadata import add_component_from_custom_property, add_component_to_object, apply_propertyGroup_values_to_object_customProperties_for_component, copy_propertyGroup_values_to_another_object, get_bevy_component_value_by_long_name, get_bevy_components, is_bevy_component_in_object, remove_component_from_object, rename_component, toggle_component
class AddComponentOperator(Operator):
"""Add Bevy component to object"""
bl_idname = "object.add_bevy_component"
bl_label = "Add component to object Operator"
bl_options = {"UNDO"}
component_type: StringProperty(
name="component_type",
description="component type to add",
) # type: ignore
def execute(self, context):
object = context.object
print("adding component ", self.component_type, "to object '"+object.name+"'")
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 Bevy component from object"""
bl_idname = "object.copy_bevy_component"
bl_label = "Copy component Operator"
bl_options = {"UNDO"}
source_component_name: StringProperty(
name="source component_name (long)",
description="name of the component to copy",
) # type: ignore
source_object_name: StringProperty(
name="source object name",
description="name of the object to copy the component from",
) # type: ignore
@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 Bevy component to object"""
bl_idname = "object.paste_bevy_component"
bl_label = "Paste component to object 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
component_value = get_bevy_component_value_by_long_name(source_object, component_name)
if component_value is None:
self.report({"ERROR"}, "The source component to copy from does not exist")
else:
print("pasting component to object: component name:", str(component_name), "component value:" + str(component_value))
print (context.object)
registry = context.window_manager.components_registry
copy_propertyGroup_values_to_another_object(source_object, context.object, component_name, registry)
return {'FINISHED'}
class RemoveComponentOperator(Operator):
"""Remove Bevy component from object"""
bl_idname = "object.remove_bevy_component"
bl_label = "Remove component from object Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to delete",
) # type: ignore
object_name: StringProperty(
name="object name",
description="object whose component to delete",
default=""
) # type: ignore
def execute(self, context):
if self.object_name == "":
object = context.object
else:
object = bpy.data.objects[self.object_name]
print("removing component ", self.component_name, "from object '"+object.name+"'")
if object is not None and 'bevy_components' in object :
component_value = get_bevy_component_value_by_long_name(object, self.component_name)
if component_value is not None:
remove_component_from_object(object, self.component_name)
else :
self.report({"ERROR"}, "The component to remove ("+ self.component_name +") does not exist")
else:
self.report({"ERROR"}, "The object to remove ("+ self.component_name +") from does not exist")
return {'FINISHED'}
class RemoveComponentFromAllObjectsOperator(Operator):
"""Remove Bevy component from all object"""
bl_idname = "object.remove_bevy_component_all"
bl_label = "Remove component from all objects Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name (long name)",
description="component to delete",
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_remove_progress = bpy.props.FloatProperty(default=-1.0)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_remove_progress
def execute(self, context):
print("removing component ", self.component_name, "from all objects")
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
if len(object.keys()) > 0:
if object is not None and is_bevy_component_in_object(object, self.component_name):
remove_component_from_object(object, self.component_name)
progress = index / total
context.window_manager.components_remove_progress = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.components_remove_progress = -1.0
return {'FINISHED'}
class RenameHelper(bpy.types.PropertyGroup):
original_name: bpy.props.StringProperty(name="") # type: ignore
new_name: bpy.props.StringProperty(name="") # type: ignore
#object: bpy.props.PointerProperty(type=bpy.types.Object)
@classmethod
def register(cls):
bpy.types.WindowManager.bevy_component_rename_helper = bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
# remove handlers & co
del bpy.types.WindowManager.bevy_component_rename_helper
class OT_rename_component(Operator):
"""Rename Bevy component"""
bl_idname = "object.rename_bevy_component"
bl_label = "rename component"
bl_options = {"UNDO"}
original_name: bpy.props.StringProperty(default="") # type: ignore
new_name: StringProperty(
name="new_name",
description="new name of component",
) # type: ignore
target_objects: bpy.props.StringProperty() # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_rename_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_rename_progress
def execute(self, context):
registry = context.window_manager.components_registry
type_infos = registry.type_infos
settings = context.window_manager.bevy_component_rename_helper
original_name = settings.original_name if self.original_name == "" else self.original_name
new_name = self.new_name
print("renaming components: original name", original_name, "new_name", self.new_name, "targets", self.target_objects)
target_objects = json.loads(self.target_objects)
errors = []
total = len(target_objects)
if original_name != '' and new_name != '' and original_name != new_name and len(target_objects) > 0:
for index, object_name in enumerate(target_objects):
object = bpy.data.objects[object_name]
if object and original_name in get_bevy_components(object) or original_name in object:
try:
# attempt conversion
rename_component(object=object, original_long_name=original_name, new_long_name=new_name)
except Exception as error:
if '__disable__update' in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
components_metadata = getattr(object, "components_meta", None)
if components_metadata:
components_metadata = components_metadata.components
component_meta = next(filter(lambda component: component["long_name"] == new_name, components_metadata), None)
if component_meta:
component_meta.invalid = True
component_meta.invalid_details = "wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate"
errors.append( "wrong custom property values to generate target component: object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_rename_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except: pass # this is to allow this to run in cli/headless mode
if len(errors) > 0:
self.report({'ERROR'}, "Failed to rename component: Errors:" + str(errors))
else:
self.report({'INFO'}, "Sucessfully renamed component")
#clear data after we are done
self.original_name = ""
context.window_manager.bevy_component_rename_helper.original_name = ""
context.window_manager.components_rename_progress = -1.0
return {'FINISHED'}
class GenerateComponent_From_custom_property_Operator(Operator):
"""Generate Bevy components from custom property"""
bl_idname = "object.generate_bevy_component_from_custom_property"
bl_label = "Generate component from custom_property Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to generate custom properties for",
) # type: ignore
def execute(self, context):
object = context.object
error = False
try:
add_component_from_custom_property(object)
except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error))
if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object")
return {'FINISHED'}
class Fix_Component_Operator(Operator):
"""Attempt to fix Bevy component"""
bl_idname = "object.fix_bevy_component"
bl_label = "Fix component (attempts to)"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to fix",
) # type: ignore
def execute(self, context):
object = context.object
error = False
try:
apply_propertyGroup_values_to_object_customProperties_for_component(object, self.component_name)
except Exception as error:
if "__disable__update" in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to fix component: Error:" + str(error))
if not error:
self.report({'INFO'}, "Sucessfully fixed component (please double check component & its custom property value)")
return {'FINISHED'}
class Toggle_ComponentVisibility(Operator):
"""Toggle Bevy component's visibility"""
bl_idname = "object.toggle_bevy_component_visibility"
bl_label = "Toggle component visibility"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to toggle",
) # type: ignore
def execute(self, context):
object = context.object
toggle_component(object, self.component_name)
return {'FINISHED'}

View File

@ -1,273 +0,0 @@
import json
import bpy
from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT
from .metadata import do_object_custom_properties_have_missing_metadata, get_bevy_components
from .operators import AddComponentOperator, CopyComponentOperator, Fix_Component_Operator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility
def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum")
is_list = getattr(propertyGroup, "with_list")
is_map = getattr(propertyGroup, "with_map")
# 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)
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, "selection")
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:
item_list = getattr(propertyGroup, "list")
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)
elif is_map:
root = layout.row().column()
if hasattr(propertyGroup, "list"): # TODO: improve handling of non drawable UI
keys_list = getattr(propertyGroup, "list")
values_list = getattr(propertyGroup, "values_list")
box = root.box()
row = box.row()
row.label(text="Add entry:")
keys_setter = getattr(propertyGroup, "keys_setter")
draw_propertyGroup(keys_setter, row, nesting, rootName)
values_setter = getattr(propertyGroup, "values_setter")
draw_propertyGroup(values_setter, row, nesting, rootName)
op = row.operator('generic_map.map_action', icon='ADD', text="")
op.action = 'ADD'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
box = root.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(keys_list):
row = list_column.row()
draw_propertyGroup(item, row, nesting, rootName)
value = values_list[index]
draw_propertyGroup(value, row, nesting, rootName)
op = row.operator('generic_map.map_action', icon='REMOVE', text="")
op.action = 'REMOVE'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
op.target_index = index
#various control buttons
buttons_column.separator()
row = buttons_column.row()
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.separator()
layout.label(text=display_name) # this is the name of the field/sub field
layout.separator()
subrow = layout.row()
draw_propertyGroup(nestedPropertyGroup, subrow, nesting + [fname], rootName )
else:
subrow = layout.row()
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
registry_has_type_infos = registry.has_type_infos()
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_has_type_infos and context.window_manager.copied_source_object != ''
layout.separator()
# upgrate custom props to components
upgradeable_customProperties = registry.has_type_infos() 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
#print("components_names", dict(components_bla).keys())
for component_name in sorted(get_bevy_components(object)) : # sorted by component name, practical
#print("component_name", component_name)
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["long_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 = registry.get_propertyGroupName_from_longName(component_name)
"""print("root_propertyGroup_name", root_propertyGroup_name)"""
print("component_meta", component_meta, component_invalid)
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
"""print("propertyGroup", propertyGroup)"""
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
if component_invalid:
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
unit_struct = len(propertyGroup.field_names) == 0
if unit_struct:
op = row.operator(Fix_Component_Operator.bl_idname, text="", icon="SHADERFX")
op.component_name = component_name
row.separator()
op = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
op.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.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,30 +0,0 @@
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
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

@ -1,178 +0,0 @@
from bpy_types import PropertyGroup
conversion_tables = {
"bool": lambda value: value,
"char": lambda value: '"'+value+'"',
"str": lambda value: '"'+value+'"',
"alloc::string::String": lambda value: '"'+str(value)+'"',
"alloc::borrow::Cow<str>": lambda value: '"'+str(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):
long_name = definition["long_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
is_value_type = long_name in conversion_tables
# print("computing custom property: component name:", long_name, "type_info", type_info, "type_def", type_def, "value", value)
if is_value_type:
value = conversion_tables[long_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_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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=long_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_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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=long_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):
#print("toto", index, definition["prefixItems"][index]["type"]["$ref"])
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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=long_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, "selection")
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=long_name, value=value)
value = selected + str(value,) #"{}{},".format(selected ,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=long_name, value=value)
value = selected + str(value,)
else:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if child_property_group:
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=long_name, value=value)
value = selected + str(value,)
else:
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")
value = []
for item in item_list:
item_long_name = getattr(item, "long_name")
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
item_value = property_group_value_to_custom_property_value(item, definition, registry, long_name, None)
if item_long_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)
elif type_info == "Map":
keys_list = getattr(property_group, "list", {})
values_list = getattr(property_group, "values_list")
value = {}
for index, key in enumerate(keys_list):
# first get the keys
key_long_name = getattr(key, "long_name")
definition = registry.type_infos[key_long_name] if key_long_name in registry.type_infos else None
if definition != None:
key_value = property_group_value_to_custom_property_value(key, definition, registry, long_name, None)
if key_long_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
key_value = key_value[0]
else:
key_value = '""'
# and then the values
val = values_list[index]
value_long_name = getattr(val, "long_name")
definition = registry.type_infos[value_long_name] if value_long_name in registry.type_infos else None
if definition != None:
val_value = property_group_value_to_custom_property_value(val, definition, registry, long_name, None)
if value_long_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
val_value = val_value[0]
else:
val_value = '""'
value[key_value] = val_value
value = str(value).replace('{','@').replace('}','²') # FIXME: eeek !!
else:
value = conversion_tables[long_name](value) if is_value_type else value
value = '""' if isinstance(value, PropertyGroup) else value
#print("generating custom property value", value, type(value))
if isinstance(value, str):
value = value.replace("'", "")
if parent == None:
value = str(value).replace("'", "")
value = value.replace(",)",")")
value = value.replace("{", "(").replace("}", ")") # FIXME: deal with hashmaps
value = value.replace("True", "true").replace("False", "false")
value = value.replace('@', '{').replace('²', '}')
return value

View File

@ -1,312 +0,0 @@
from bpy_types import PropertyGroup
import re
def parse_struct_string(string, start_nesting=0):
#print("processing struct string", string, "start_nesting", start_nesting)
fields = {}
buff = []
current_fieldName = None
nesting_level = 0
start_offset = 0
end_offset = 0
for index, char in enumerate(string):
buff.append(char)
if char == "," and nesting_level == start_nesting:
#print("first case", end_offset)
end_offset = index
end_offset = len(string) if end_offset == 0 else end_offset
val = "".join(string[start_offset:end_offset])
fields[current_fieldName] = val.strip()
start_offset = index + 1
#print("done with field name", current_fieldName, "value", fields[current_fieldName])
if char == "[" or char == "(":
nesting_level += 1
if nesting_level == start_nesting:
start_offset = index + 1
#print("nesting & setting start offset", start_offset)
#print("nesting down", nesting_level)
if char == "]" or char == ")" :
#print("nesting up", nesting_level)
if nesting_level == start_nesting:
end_offset = index
#print("unesting & setting end offset", end_offset)
nesting_level -= 1
if char == ":" and nesting_level == start_nesting:
end_offset = index
fieldName = "".join(string[start_offset:end_offset])
current_fieldName = fieldName.strip()
start_offset = index + 1
end_offset = 0 #hack
#print("starting field name", fieldName, "index", index)
buff = []
end_offset = len(string) if end_offset == 0 else end_offset
#print("final start and end offset", start_offset, end_offset, "total length", len(string))
val = "".join(string[start_offset:end_offset])
fields[current_fieldName] = val.strip()
#print("done with all fields", fields)
return fields
def parse_tuplestruct_string(string, start_nesting=0):
#print("processing tuppleStruct", string, "start_nesting", start_nesting)
fields = []
buff = []
nesting_level = 0
field_index = 0
start_offset = 0
end_offset = 0
# todo: strip all stuff before start_nesting
for index, char in enumerate(string):
buff.append(char)
if char == "," and nesting_level == start_nesting:
end_offset = index
end_offset = len(string) if end_offset == 0 else end_offset
val = "".join(string[start_offset:end_offset])
fields.append(val.strip())
field_index += 1
#print("start and end offset", start_offset, end_offset, "total length", len(string))
#print("done with field name", field_index, "value", fields)
start_offset = index + 1
end_offset = 0 # hack
if char == "[" or char == "(":
nesting_level += 1
if nesting_level == start_nesting:
start_offset = index + 1
#print("nesting & setting start offset", start_offset)
#print("nesting down", nesting_level)
if char == "]" or char == ")" :
if nesting_level == start_nesting:
end_offset = index
#print("unesting & setting end offset", end_offset)
#print("nesting up", nesting_level)
nesting_level -= 1
end_offset = len(string) if end_offset == 0 else end_offset
#print("final start and end offset", start_offset, end_offset, "total length", len(string))
val = "".join(string[start_offset:end_offset]) #if end_offset != 0 else buff)
fields.append(val.strip())
fields = list(filter(lambda entry: entry != '', fields))
#print("done with all fields", fields)
return fields
def parse_vec2(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y'])]
def parse_vec3(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y']), caster(parsed['z'])]
def parse_vec4(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y']), caster(parsed['z']), caster(parsed['w'])]
def parse_color(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['red']), caster(parsed['green']), caster(parsed['blue']), caster(parsed['alpha'])]
def to_int(input):
return int(float(input))
type_mappings = {
"bool": lambda value: True if value == "true" else False,
"u8": lambda value: int(value),
"u16": lambda value: int(value),
"u32": lambda value: int(value),
"u64": lambda value: int(value),
"u128": lambda value: int(value),
"u64": lambda value: int(value),
"usize": lambda value: int(value),
"i8": lambda value: int(value),
"i16": lambda value: int(value),
"i32": lambda value: int(value),
"i64": lambda value: int(value),
"i128": lambda value: int(value),
"isize": lambda value: int(value),
'f32': lambda value: float(value),
'f64': lambda value: float(value),
"glam::Vec2": lambda value: parse_vec2(value, float, "Vec2"),
"glam::DVec2": lambda value: parse_vec2(value, float, "DVec2"),
"glam::UVec2": lambda value: parse_vec2(value, to_int, "UVec2"),
'glam::Vec3': lambda value: parse_vec3(value, float, "Vec3"),
"glam::Vec3A": lambda value: parse_vec3(value, float, "Vec3A"),
"glam::UVec3": lambda value: parse_vec3(value, to_int, "UVec3"),
"glam::Vec4": lambda value: parse_vec4(value, float, "Vec4"),
"glam::DVec4": lambda value: parse_vec4(value, float, "DVec4"),
"glam::UVec4": lambda value: parse_vec4(value, to_int, "UVec4"),
"glam::Quat": lambda value: parse_vec4(value, float, "Quat"),
'alloc::string::String': lambda value: str(value.replace('"', "")),
'alloc::borrow::Cow<str>': lambda value: str(value.replace('"', "")),
'bevy_render::color::Color': lambda value: parse_color(value, float, "Rgba"),
'bevy_ecs::entity::Entity': lambda value: int(value),
}
def is_def_value_type(definition, registry):
if definition == None:
return True
value_types_defaults = registry.value_types_defaults
long_name = definition["long_name"]
is_value_type = long_name in value_types_defaults
return is_value_type
#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, value, nesting = []):
value_types_defaults = registry.value_types_defaults
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 []
long_name = definition["long_name"]
#is_value_type = type_def in value_types_defaults or long_name in value_types_defaults
is_value_type = long_name in value_types_defaults
nesting = nesting + [definition["short_name"]]
if is_value_type:
value = value.replace("(", "").replace(")", "")# FIXME: temporary, incoherent use of nesting levels between parse_tuplestruct_string & parse_struct_string
value = type_mappings[long_name](value) if long_name in type_mappings else value
return value
elif type_info == "Struct":
if len(property_group.field_names) != 0 :
custom_property_values = parse_struct_string(value, start_nesting=1 if value.startswith("(") else 0)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_prop_value = custom_property_values[field_name]
#print("field name", field_name, "value", custom_prop_value)
propGroup_value = getattr(property_group, field_name)
is_property_group = isinstance(propGroup_value, PropertyGroup)
child_property_group = propGroup_value if is_property_group else None
if item_definition != None:
custom_prop_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_prop_value, nesting=nesting)
else:
custom_prop_value = custom_prop_value
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_prop_value)
else:
if len(value) > 2: #a unit struct should be two chars long :()
#print("struct with zero fields")
raise Exception("input string too big for a unit struct")
elif type_info == "Tuple":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_property_value = custom_property_values[index]
propGroup_value = getattr(property_group, field_name)
is_property_group = isinstance(propGroup_value, PropertyGroup)
child_property_group = propGroup_value if is_property_group else None
if item_definition != None:
custom_property_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_property_value, nesting=nesting)
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_property_value)
elif type_info == "TupleStruct":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 0)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_prop_value = custom_property_values[index]
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:
custom_prop_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_prop_value, nesting=nesting)
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_prop_value)
elif type_info == "Enum":
field_names = property_group.field_names
if type_def == "object":
regexp = re.search('(^[^\(]+)(\((.*)\))', value)
try:
chosen_variant_raw = regexp.group(1)
chosen_variant_value = regexp.group(3)
chosen_variant_name = "variant_" + chosen_variant_raw
except:
chosen_variant_raw = value
chosen_variant_value = ""
chosen_variant_name = "variant_" + chosen_variant_raw
selection_index = property_group.field_names.index(chosen_variant_name)
variant_definition = definition["oneOf"][selection_index-1]
# first we set WHAT variant is selected
setattr(property_group, "selection", chosen_variant_raw)
# and then we set the value of the variant
if "prefixItems" in variant_definition:
value = getattr(property_group, chosen_variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
chosen_variant_value = "(" +chosen_variant_value +")" # needed to handle nesting correctly
value = property_group_value_from_custom_property_value(child_property_group, variant_definition, registry, value=chosen_variant_value, nesting=nesting)
elif "properties" in variant_definition:
value = getattr(property_group, chosen_variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_from_custom_property_value(child_property_group, variant_definition, registry, value=chosen_variant_value, nesting=nesting)
else:
chosen_variant_raw = value
setattr(property_group, field_names[0], chosen_variant_raw)
elif type_info == "List":
item_list = getattr(property_group, "list")
item_long_name = getattr(property_group, "long_name")
custom_property_values = parse_tuplestruct_string(value, start_nesting=2 if item_long_name.startswith("wrapper_") and value.startswith('(') else 1) # TODO : the additional check here is wrong, there is an issue somewhere in higher level stuff
# clear list first
item_list.clear()
for raw_value in custom_property_values:
new_entry = item_list.add()
item_long_name = getattr(new_entry, "long_name") # we get the REAL type name
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
property_group_value_from_custom_property_value(new_entry, definition, registry, value=raw_value, nesting=nesting)
else:
try:
value = value.replace("(", "").replace(")", "")# FIXME: temporary, incoherent use of nesting levels between parse_tuplestruct_string & parse_struct_string
value = type_mappings[long_name](value) if long_name in type_mappings else value
return value
except:
pass

View File

@ -1,95 +0,0 @@
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
from . import process_map
def process_component(registry, definition, update, extras=None, nesting = [], nesting_long_names = []):
long_name = definition['long_name']
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"
is_map = type_info == "Map"
__annotations__ = {}
tupple_or_struct = None
with_properties = False
with_items = False
with_enum = False
with_list = False
with_map = False
if has_properties:
__annotations__ = __annotations__ | process_structs.process_structs(registry, definition, properties, update, nesting, nesting_long_names)
with_properties = True
tupple_or_struct = "struct"
if has_prefixItems:
__annotations__ = __annotations__ | process_tupples.process_tupples(registry, definition, prefixItems, update, nesting, nesting_long_names)
with_items = True
tupple_or_struct = "tupple"
if is_enum:
__annotations__ = __annotations__ | process_enum.process_enum(registry, definition, update, nesting, nesting_long_names)
with_enum = True
if is_list:
__annotations__ = __annotations__ | process_list.process_list(registry, definition, update, nesting, nesting_long_names)
with_list= True
if is_map:
__annotations__ = __annotations__ | process_map.process_map(registry, definition, update, nesting, nesting_long_names)
with_map = True
field_names = []
for a in __annotations__:
field_names.append(a)
extras = extras if extras is not None else {
"long_name": long_name
}
root_component = nesting_long_names[0] if len(nesting_long_names) > 0 else long_name
# print("")
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, with_map = with_map, short_name= short_name, long_name=long_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 = registry.generate_propGroup_name(nesting, long_name)
(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)
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)

View File

@ -1,67 +0,0 @@
from bpy.props import (StringProperty)
from . import process_component
def process_enum(registry, definition, update, nesting, nesting_long_names):
blender_property_mapping = registry.blender_property_mapping
short_name = definition["short_name"]
long_name = definition["long_name"]
type_def = definition["type"] if "type" in definition else None
variants = definition["oneOf"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names = [long_name]
__annotations__ = {}
original_type_name = "enum"
# print("processing enum", short_name, long_name, definition)
if type_def == "object":
labels = []
additional_annotations = {}
for variant in variants:
variant_name = variant["long_name"]
variant_prefixed_name = "variant_" + variant_name
labels.append(variant_name)
if "prefixItems" in variant:
#print("tupple variant in enum", variant)
registry.add_custom_type(variant_name, variant)
(sub_component_group, _) = process_component.process_component(registry, variant, update, {"nested": True}, nesting, nesting_long_names)
additional_annotations[variant_prefixed_name] = sub_component_group
elif "properties" in variant:
#print("struct variant in enum", variant)
registry.add_custom_type(variant_name, variant)
(sub_component_group, _) = process_component.process_component(registry, variant, update, {"nested": True}, nesting, nesting_long_names)
additional_annotations[variant_prefixed_name] = sub_component_group
else: # for the cases where it's neither a tupple nor a structs: FIXME: not 100% sure of this
#print("other variant in enum")
annotations = {"variant_"+variant_name: StringProperty(default="----<ignore_field>----")}
additional_annotations = additional_annotations | annotations
items = tuple((e, e, e) for e in labels)
blender_property_def = blender_property_mapping[original_type_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
items=items, # this is needed by Blender's EnumProperty , which we are using here
update= update
)
__annotations__["selection"] = blender_property
for a in additional_annotations:
__annotations__[a] = additional_annotations[a]
# enum_value => what field to display
# a second field + property for the "content" of the enum
else:
items = tuple((e, e, "") for e in variants)
blender_property_def = blender_property_mapping[original_type_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
items=items,
update= update
)
__annotations__["selection"] = blender_property
return __annotations__

View File

@ -1,37 +0,0 @@
from bpy.props import (StringProperty, IntProperty, CollectionProperty)
from .utils import generate_wrapper_propertyGroup
from . import process_component
def process_list(registry, definition, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
type_infos = registry.type_infos
short_name = definition["short_name"]
long_name = definition["long_name"]
ref_name = definition["items"]["type"]["$ref"].replace("#/$defs/", "")
nesting = nesting+[short_name]
nesting_long_names = nesting_long_names + [long_name]
item_definition = type_infos[ref_name]
item_long_name = item_definition["long_name"]
is_item_value_type = item_long_name in value_types_defaults
property_group_class = None
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_item_value_type:
property_group_class = generate_wrapper_propertyGroup(long_name, item_long_name, definition["items"]["type"]["$ref"], registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, item_definition, update, {"nested": True, "long_name": item_long_name}, nesting)
property_group_class = list_content_group_class
item_collection = CollectionProperty(type=property_group_class)
item_long_name = item_long_name if not is_item_value_type else "wrapper_" + item_long_name
__annotations__ = {
"list": item_collection,
"list_index": IntProperty(name = "Index for list", default = 0, update=update),
"long_name": StringProperty(default=item_long_name)
}
return __annotations__

View File

@ -1,85 +0,0 @@
from bpy.props import (StringProperty, IntProperty, CollectionProperty, PointerProperty)
from .utils import generate_wrapper_propertyGroup
from . import process_component
def process_map(registry, definition, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
type_infos = registry.type_infos
short_name = definition["short_name"]
long_name = definition["long_name"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
value_ref_name = definition["valueType"]["type"]["$ref"].replace("#/$defs/", "")
key_ref_name = definition["keyType"]["type"]["$ref"].replace("#/$defs/", "")
#print("definition", definition)
__annotations__ = {}
if key_ref_name in type_infos:
key_definition = type_infos[key_ref_name]
original_long_name = key_definition["long_name"]
is_key_value_type = original_long_name in value_types_defaults
definition_link = definition["keyType"]["type"]["$ref"]
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_key_value_type:
keys_property_group_class = generate_wrapper_propertyGroup(f"{long_name}_keys", original_long_name, definition_link, registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, key_definition, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
keys_property_group_class = list_content_group_class
keys_collection = CollectionProperty(type=keys_property_group_class)
keys_property_group_pointer = PointerProperty(type=keys_property_group_class)
else:
__annotations__["list"] = StringProperty(default="N/A")
registry.add_missing_typeInfo(key_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
if value_ref_name in type_infos:
value_definition = type_infos[value_ref_name]
original_long_name = value_definition["long_name"]
is_value_value_type = original_long_name in value_types_defaults
definition_link = definition["valueType"]["type"]["$ref"]
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_value_value_type:
values_property_group_class = generate_wrapper_propertyGroup(f"{long_name}_values", original_long_name, definition_link, registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, value_definition, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
values_property_group_class = list_content_group_class
values_collection = CollectionProperty(type=values_property_group_class)
values_property_group_pointer = PointerProperty(type=values_property_group_class)
else:
#__annotations__["list"] = StringProperty(default="N/A")
registry.add_missing_typeInfo(value_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
if key_ref_name in type_infos and value_ref_name in type_infos:
__annotations__ = {
"list": keys_collection,
"list_index": IntProperty(name = "Index for keys", default = 0, update=update),
"keys_setter":keys_property_group_pointer,
"values_list": values_collection,
"values_list_index": IntProperty(name = "Index for values", default = 0, update=update),
"values_setter":values_property_group_pointer,
}
"""__annotations__["list"] = StringProperty(default="N/A")
__annotations__["values_list"] = StringProperty(default="N/A")
__annotations__["keys_setter"] = StringProperty(default="N/A")"""
"""registry.add_missing_typeInfo(key_ref_name)
registry.add_missing_typeInfo(value_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
print("setting invalid flag for", nesting_long_names[0])"""
return __annotations__

View File

@ -1,48 +0,0 @@
from bpy.props import (StringProperty)
from . import process_component
def process_structs(registry, definition, properties, update, nesting, nesting_long_names):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
type_infos = registry.type_infos
long_name = definition["long_name"]
short_name = definition["short_name"]
__annotations__ = {}
default_values = {}
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
for property_name in properties.keys():
ref_name = properties[property_name]["type"]["$ref"].replace("#/$defs/", "")
if ref_name in type_infos:
original = type_infos[ref_name]
original_long_name = original["long_name"]
is_value_type = original_long_name in value_types_defaults
value = value_types_defaults[original_long_name] if is_value_type else None
default_values[property_name] = value
if is_value_type:
if original_long_name in blender_property_mapping:
blender_property_def = blender_property_mapping[original_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = property_name,
default = value,
update = update
)
__annotations__[property_name] = blender_property
else:
original_long_name = original["long_name"]
(sub_component_group, _) = process_component.process_component(registry, original, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
__annotations__[property_name] = sub_component_group
# if there are sub fields, add an attribute "sub_fields" possibly a pointer property ? or add a standard field to the type , that is stored under "attributes" and not __annotations (better)
else:
# component not found in type_infos, generating placeholder
__annotations__[property_name] = StringProperty(default="N/A")
registry.add_missing_typeInfo(ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
return __annotations__

View File

@ -1,55 +0,0 @@
from bpy.props import (StringProperty)
from . import process_component
def process_tupples(registry, definition, prefixItems, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
type_infos = registry.type_infos
long_name = definition["long_name"]
short_name = definition["short_name"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
__annotations__ = {}
default_values = []
prefix_infos = []
for index, item in enumerate(prefixItems):
ref_name = item["type"]["$ref"].replace("#/$defs/", "")
property_name = str(index)# we cheat a bit, property names are numbers here, as we do not have a real property name
if ref_name in type_infos:
original = type_infos[ref_name]
original_long_name = original["long_name"]
is_value_type = original_long_name in value_types_defaults
value = value_types_defaults[original_long_name] if is_value_type else None
default_values.append(value)
prefix_infos.append(original)
if is_value_type:
if original_long_name in blender_property_mapping:
blender_property_def = blender_property_mapping[original_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = property_name,
default=value,
update= update
)
__annotations__[property_name] = blender_property
else:
original_long_name = original["long_name"]
(sub_component_group, _) = process_component.process_component(registry, original, update, {"nested": True, "long_name": original_long_name}, nesting)
__annotations__[property_name] = sub_component_group
else:
# component not found in type_infos, generating placeholder
__annotations__[property_name] = StringProperty(default="N/A")
registry.add_missing_typeInfo(ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
return __annotations__

View File

@ -1,44 +0,0 @@
import bpy
from .conversions_from_prop_group import property_group_value_to_custom_property_value
from .process_component import process_component
from .utils import update_calback_helper
import json
## main callback function, fired whenever any property changes, no matter the nesting level
def update_component(self, context, definition, component_name):
registry = bpy.context.window_manager.components_registry
current_object = bpy.context.object
update_disabled = current_object["__disable__update"] if "__disable__update" in current_object else False
update_disabled = registry.disable_all_object_updates or update_disabled # global settings
if update_disabled:
return
print("")
print("update in component", component_name, self, "current_object", current_object.name)
components_in_object = current_object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_in_object), None)
if component_meta != None:
property_group_name = registry.get_propertyGroupName_from_longName(component_name)
property_group = getattr(component_meta, property_group_name)
# we use our helper to set the values
object = context.object
previous = json.loads(object['bevy_components'])
previous[component_name] = property_group_value_to_custom_property_value(property_group, definition, registry, None)
object['bevy_components'] = json.dumps(previous)
def generate_propertyGroups_for_components():
registry = bpy.context.window_manager.components_registry
if not registry.has_type_infos():
registry.load_type_infos()
type_infos = registry.type_infos
for component_name in type_infos:
definition = type_infos[component_name]
is_component = definition['isComponent'] if "isComponent" in definition else False
root_property_name = component_name if is_component else None
process_component(registry, definition, update_calback_helper(definition, update_component, root_property_name), None, [])
# if we had to add any wrapper types on the fly, process them now
registry.process_custom_types()

View File

@ -1,63 +0,0 @@
# helper function that returns a lambda, used for the PropertyGroups update function
def update_calback_helper(definition, update, component_name_override):
return lambda self, context: update(self, context, definition, component_name_override)
import bpy
from bpy.props import (StringProperty)
from bpy_types import PropertyGroup
# this helper creates a "fake"/wrapper property group that is NOT a real type in the registry
# usefull for things like value types in list items etc
def generate_wrapper_propertyGroup(wrapped_type_long_name_name, item_long_name, definition_link, registry, update):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
is_item_value_type = item_long_name in value_types_defaults
wrapper_name = "wrapper_" + wrapped_type_long_name_name
wrapper_definition = {
"isComponent": False,
"isResource": False,
"items": False,
"prefixItems": [
{
"type": {
"$ref": definition_link
}
}
],
"short_name": wrapper_name, # FIXME !!!
"long_name": wrapper_name,
"type": "array",
"typeInfo": "TupleStruct"
}
# we generate a very small 'hash' for the component name
property_group_name = registry.generate_propGroup_name(nesting=[], longName=wrapper_name)
registry.add_custom_type(wrapper_name, wrapper_definition)
blender_property = StringProperty(default="", update=update)
if item_long_name in blender_property_mapping:
value = value_types_defaults[item_long_name] if is_item_value_type else None
blender_property_def = blender_property_mapping[item_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = "property_name",
default = value,
update = update
)
wrapper_annotations = {
'0' : blender_property
}
property_group_params = {
'__annotations__': wrapper_annotations,
'tupple_or_struct': "tupple",
'field_names': ['0'],
**dict(with_properties = False, with_items= True, with_enum= False, with_list= False, with_map =False, short_name=wrapper_name, long_name=wrapper_name),
}
property_group_class = type(property_group_name, (PropertyGroup,), property_group_params)
bpy.utils.register_class(property_group_class)
return property_group_class

View File

@ -1,3 +0,0 @@
[pytest]
testpaths =
tests

View File

@ -1,237 +0,0 @@
import os
import bpy
from bpy_types import (Operator)
from bpy.props import (StringProperty)
from bpy_extras.io_utils import ImportHelper
from ..helpers import upsert_settings
from ..components.metadata import apply_customProperty_values_to_object_propertyGroups, apply_propertyGroup_values_to_object_customProperties, ensure_metadata_for_all_objects
from ..propGroups.prop_groups import generate_propertyGroups_for_components
class ReloadRegistryOperator(Operator):
"""Reloads registry (schema file) from disk, generates propertyGroups for components & ensures all objects have metadata """
bl_idname = "object.reload_registry"
bl_label = "Reload Registry"
bl_options = {"UNDO"}
component_type: StringProperty(
name="component_type",
description="component type to add",
) # type: ignore
def execute(self, context):
print("reload registry")
context.window_manager.components_registry.load_schema()
generate_propertyGroups_for_components()
print("")
print("")
print("")
ensure_metadata_for_all_objects()
# now force refresh the ui
for area in context.screen.areas:
for region in area.regions:
if region.type == "UI":
region.tag_redraw()
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL(Operator):
"""Apply registry to ALL objects: update the custom property values of all objects based on their definition, if any"""
bl_idname = "object.refresh_custom_properties_all"
bl_label = "Apply Registry to all objects"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress_all
def execute(self, context):
print("apply registry to all")
#context.window_manager.components_registry.load_schema()
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
apply_propertyGroup_values_to_object_customProperties(object)
progress = index / total
context.window_manager.custom_properties_from_components_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.custom_properties_from_components_progress_all = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT(Operator):
"""Apply registry to CURRENT object: update the custom property values of current object based on their definition, if any"""
bl_idname = "object.refresh_custom_properties_current"
bl_label = "Apply Registry to current object"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress
def execute(self, context):
print("apply registry to current object")
object = context.object
context.window_manager.custom_properties_from_components_progress = 0.5
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
apply_propertyGroup_values_to_object_customProperties(object)
context.window_manager.custom_properties_from_components_progress = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT(Operator):
"""Update UI values from custom properties to CURRENT object"""
bl_idname = "object.refresh_ui_from_custom_properties_current"
bl_label = "Apply custom_properties to current object"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress
def execute(self, context):
print("apply custom properties to current object")
object = context.object
error = False
try:
apply_customProperty_values_to_object_propertyGroups(object)
progress = 0.5
context.window_manager.components_from_custom_properties_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except:pass # ony run in ui
except Exception as error_message:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error_message))
if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object")
context.window_manager.components_from_custom_properties_progress = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator):
"""Update UI values from custom properties to ALL object"""
bl_idname = "object.refresh_ui_from_custom_properties_all"
bl_label = "Apply custom_properties to all objects"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress_all
def execute(self, context):
print("apply custom properties to all object")
bpy.context.window_manager.components_registry.disable_all_object_updates = True
errors = []
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
try:
apply_customProperty_values_to_object_propertyGroups(object)
except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
errors.append( "object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_from_custom_properties_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
if len(errors) > 0:
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors))
else:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects")
bpy.context.window_manager.components_registry.disable_all_object_updates = False
context.window_manager.components_from_custom_properties_progress_all = -1.0
return {'FINISHED'}
class OT_OpenFilebrowser(Operator, ImportHelper):
"""Browse for registry json file"""
bl_idname = "generic.open_filebrowser"
bl_label = "Open the file browser"
filter_glob: StringProperty(
default='*.json',
options={'HIDDEN'}
) # type: ignore
def execute(self, context):
"""Do something with the selected file(s)."""
#filename, extension = os.path.splitext(self.filepath)
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
relative_path = os.path.relpath(self.filepath, folder_path)
registry = context.window_manager.components_registry
registry.schemaPath = relative_path
upsert_settings(registry.settings_save_path, {"schemaPath": relative_path})
return {'FINISHED'}
class OT_select_object(Operator):
"""Select object by name"""
bl_idname = "object.select"
bl_label = "Select object"
bl_options = {"UNDO"}
object_name: StringProperty(
name="object_name",
description="object to select's name ",
) # type: ignore
def execute(self, context):
if self.object_name:
object = bpy.data.objects[self.object_name]
scenes_of_object = list(object.users_scene)
if len(scenes_of_object) > 0:
bpy.ops.object.select_all(action='DESELECT')
bpy.context.window.scene = scenes_of_object[0]
object.select_set(True)
bpy.context.view_layer.objects.active = object
return {'FINISHED'}
class OT_select_component_name_to_replace(Operator):
"""Select component name to replace"""
bl_idname = "object.select_component_name_to_replace"
bl_label = "Select component name for bulk replace"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component_name",
description="component name to replace",
) # type: ignore
def execute(self, context):
context.window_manager.bevy_component_rename_helper.original_name = self.component_name
return {'FINISHED'}

View File

@ -1,363 +0,0 @@
import bpy
import json
import os
import uuid
from pathlib import Path
from bpy_types import (PropertyGroup)
from bpy.props import (StringProperty, BoolProperty, FloatProperty, FloatVectorProperty, IntProperty, IntVectorProperty, EnumProperty, PointerProperty, CollectionProperty)
from ..helpers import load_settings
from ..propGroups.prop_groups import generate_propertyGroups_for_components
from ..components.metadata import ComponentMetadata, ensure_metadata_for_all_objects
# helper class to store missing bevy types information
class MissingBevyType(bpy.types.PropertyGroup):
long_name: bpy.props.StringProperty(
name="type",
) # type: ignore
# helper function to deal with timer
def toggle_watcher(self, context):
#print("toggling watcher", self.watcher_enabled, watch_schema, self, bpy.app.timers)
if not self.watcher_enabled:
try:
bpy.app.timers.unregister(watch_schema)
except Exception as error:
pass
else:
self.watcher_active = True
bpy.app.timers.register(watch_schema)
def watch_schema():
self = bpy.context.window_manager.components_registry
# print("watching schema file for changes")
try:
stamp = os.stat(self.schemaFullPath).st_mtime
stamp = str(stamp)
if stamp != self.schemaTimeStamp and self.schemaTimeStamp != "":
print("FILE CHANGED !!", stamp, self.schemaTimeStamp)
# see here for better ways : https://stackoverflow.com/questions/11114492/check-if-a-file-is-not-open-nor-being-used-by-another-process
"""try:
os.rename(path, path)
#return False
except OSError: # file is in use
print("in use")
#return True"""
#bpy.ops.object.reload_registry()
# we need to add an additional delay as the file might not have loaded yet
bpy.app.timers.register(lambda: bpy.ops.object.reload_registry(), first_interval=1)
self.schemaTimeStamp = stamp
except Exception as error:
pass
return self.watcher_poll_frequency if self.watcher_enabled else None
# this is where we store the information for all available components
class ComponentsRegistry(PropertyGroup):
settings_save_path = ".bevy_components_settings" # where to store data in bpy.texts
schemaPath: bpy.props.StringProperty(
name="schema path",
description="path to the registry schema file",
default="registry.json"
)# type: ignore
schemaFullPath : bpy.props.StringProperty(
name="schema full path",
description="path to the registry schema file",
)# type: ignore
registry: bpy.props. StringProperty(
name="registry",
description="component registry"
)# type: ignore
missing_type_infos: StringProperty(
name="missing type infos",
description="unregistered/missing type infos"
)# type: ignore
disable_all_object_updates: BoolProperty(name="disable_object_updates", default=False) # type: ignore
## file watcher
watcher_enabled: BoolProperty(name="Watcher_enabled", default=True, update=toggle_watcher)# type: ignore
watcher_active: BoolProperty(name = "Flag for watcher status", default = False)# type: ignore
watcher_poll_frequency: IntProperty(
name="watcher poll frequency",
description="frequency (s) at wich to poll for changes to the registry file",
min=1,
max=10,
default=1
)# type: ignore
schemaTimeStamp: StringProperty(
name="last timestamp of schema file",
description="",
default=""
)# type: ignore
missing_types_list: CollectionProperty(name="missing types list", type=MissingBevyType)# type: ignore
missing_types_list_index: IntProperty(name = "Index for missing types list", default = 0)# type: ignore
blender_property_mapping = {
"bool": dict(type=BoolProperty, presets=dict()),
"u8": dict(type=IntProperty, presets=dict(min=0, max=255)),
"u16": dict(type=IntProperty, presets=dict(min=0, max=65535)),
"u32": dict(type=IntProperty, presets=dict(min=0)),
"u64": dict(type=IntProperty, presets=dict(min=0)),
"u128": dict(type=IntProperty, presets=dict(min=0)),
"u64": dict(type=IntProperty, presets=dict(min=0)),
"usize": dict(type=IntProperty, presets=dict(min=0)),
"i8": dict(type=IntProperty, presets=dict()),
"i16":dict(type=IntProperty, presets=dict()),
"i32":dict(type=IntProperty, presets=dict()),
"i64":dict(type=IntProperty, presets=dict()),
"i128":dict(type=IntProperty, presets=dict()),
"isize": dict(type=IntProperty, presets=dict()),
"f32": dict(type=FloatProperty, presets=dict()),
"f64": dict(type=FloatProperty, presets=dict()),
"glam::Vec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::DVec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::UVec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::Vec3": {"type": FloatVectorProperty, "presets": {"size":3} },
"glam::Vec3A":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::DVec3":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::UVec3":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::Vec4": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::Vec4A": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::DVec4": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::UVec4":{"type": FloatVectorProperty, "presets": {"size":4, "min":0.0} },
"glam::Quat": {"type": FloatVectorProperty, "presets": {"size":4} },
"bevy_render::color::Color": dict(type = FloatVectorProperty, presets=dict(subtype='COLOR', size=4)),
"char": dict(type=StringProperty, presets=dict()),
"str": dict(type=StringProperty, presets=dict()),
"alloc::string::String": dict(type=StringProperty, presets=dict()),
"alloc::borrow::Cow<str>": dict(type=StringProperty, presets=dict()),
"enum": dict(type=EnumProperty, presets=dict()),
'bevy_ecs::entity::Entity': {"type": IntProperty, "presets": {"min":0} },
'bevy_utils::Uuid': dict(type=StringProperty, presets=dict()),
}
value_types_defaults = {
"string":" ",
"boolean": True,
"float": 0.0,
"uint": 0,
"int":0,
# todo : we are re-doing the work of the bevy /rust side here, but it seems more pratical to alway look for the same field name on the blender side for matches
"bool": True,
"u8": 0,
"u16":0,
"u32":0,
"u64":0,
"u128":0,
"usize":0,
"i8": 0,
"i16":0,
"i32":0,
"i64":0,
"i128":0,
"isize":0,
"f32": 0.0,
"f64":0.0,
"char": " ",
"str": " ",
"alloc::string::String": " ",
"alloc::borrow::Cow<str>": " ",
"glam::Vec2": [0.0, 0.0],
"glam::DVec2": [0.0, 0.0],
"glam::UVec2": [0, 0],
"glam::Vec3": [0.0, 0.0, 0.0],
"glam::Vec3A":[0.0, 0.0, 0.0],
"glam::UVec3": [0, 0, 0],
"glam::Vec4": [0.0, 0.0, 0.0, 0.0],
"glam::DVec4": [0.0, 0.0, 0.0, 0.0],
"glam::UVec4": [0, 0, 0, 0],
"glam::Quat": [0.0, 0.0, 0.0, 0.0],
"bevy_render::color::Color": [1.0, 1.0, 0.0, 1.0],
'bevy_ecs::entity::Entity': 0,#4294967295, # this is the same as Bevy's Entity::Placeholder, too big for Blender..sigh
'bevy_utils::Uuid': '"'+str(uuid.uuid4())+'"'
}
type_infos = {}
type_infos_missing = []
component_propertyGroups = {}
custom_types_to_add = {}
invalid_components = []
@classmethod
def register(cls):
bpy.types.WindowManager.components_registry = PointerProperty(type=ComponentsRegistry)
bpy.context.window_manager.components_registry.watcher_active = False
@classmethod
def unregister(cls):
bpy.context.window_manager.components_registry.watcher_active = False
for propgroup_name in cls.component_propertyGroups.keys():
try:
delattr(ComponentMetadata, propgroup_name)
#print("unregistered propertyGroup", propgroup_name)
except Exception as error:
pass
#print("failed to remove", error, "ComponentMetadata")
try:
bpy.app.timers.unregister(watch_schema)
except Exception as error:
pass
del bpy.types.WindowManager.components_registry
def load_schema(self):
print("load schema", self)
# cleanup previous data if any
self.propGroupIdCounter = 0
self.long_names_to_propgroup_names.clear()
self.missing_types_list.clear()
self.type_infos.clear()
self.type_infos_missing.clear()
self.component_propertyGroups.clear()
self.custom_types_to_add.clear()
self.invalid_components.clear()
# now prepare paths to load data
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
path = os.path.join(folder_path, self.schemaPath)
self.schemaFullPath = path
f = Path(bpy.path.abspath(path)) # make a path object of abs path
with open(path) as f:
data = json.load(f)
defs = data["$defs"]
self.registry = json.dumps(defs) # FIXME:meh ?
# start timer
if not self.watcher_active and self.watcher_enabled:
self.watcher_active = True
print("registering function", watch_schema)
bpy.app.timers.register(watch_schema)
# we load the json once, so we do not need to do it over & over again
def load_type_infos(self):
print("load type infos")
ComponentsRegistry.type_infos = json.loads(self.registry)
def has_type_infos(self):
return len(self.type_infos.keys()) != 0
def load_settings(self):
print("loading settings")
settings = load_settings(self.settings_save_path)
if settings!= None:
print("settings", settings)
self.schemaPath = settings["schemaPath"]
self.load_schema()
generate_propertyGroups_for_components()
ensure_metadata_for_all_objects()
# we keep a list of component propertyGroup around
def register_component_propertyGroup(self, name, propertyGroup):
self.component_propertyGroups[name] = propertyGroup
# to be able to give the user more feedback on any missin/unregistered types in their schema file
def add_missing_typeInfo(self, long_name):
if not long_name in self.type_infos_missing:
self.type_infos_missing.append(long_name)
setattr(self, "missing_type_infos", str(self.type_infos_missing))
item = self.missing_types_list.add()
item.long_name = long_name
def add_custom_type(self, long_name, type_definition):
self.custom_types_to_add[long_name] = type_definition
def process_custom_types(self):
for long_name in self.custom_types_to_add:
self.type_infos[long_name] = self.custom_types_to_add[long_name]
self.custom_types_to_add.clear()
# add an invalid component to the list (long name)
def add_invalid_component(self, component_name):
self.invalid_components.append(component_name)
###########
propGroupIdCounter: IntProperty(
name="propGroupIdCounter",
description="",
min=0,
max=1000000000,
default=0
) # type: ignore
long_names_to_propgroup_names = {}
# generate propGroup name from nesting level & shortName: each shortName + nesting is unique
def generate_propGroup_name(self, nesting, longName):
#print("gen propGroup name for", shortName, nesting)
self.propGroupIdCounter += 1
propGroupIndex = str(self.propGroupIdCounter)
propGroupName = propGroupIndex + "_ui"
key = str(nesting) + longName if len(nesting) > 0 else longName
self.long_names_to_propgroup_names[key] = propGroupName
return propGroupName
def get_propertyGroupName_from_longName(self, longName):
return self.long_names_to_propgroup_names.get(longName, None)
def long_name_to_key():
pass
###########
"""
object[component_definition.name] = 0.5
property_manager = object.id_properties_ui(component_definition.name)
property_manager.update(min=-10, max=10, soft_min=-5, soft_max=5)
print("property_manager", property_manager)
object[component_definition.name] = [0.8,0.2,1.0]
property_manager = object.id_properties_ui(component_definition.name)
property_manager.update(subtype='COLOR')
#IDPropertyUIManager
#rna_ui = object[component_definition.name].get('_RNA_UI')
"""

View File

@ -1,327 +0,0 @@
import json
import bpy
from bpy_types import (UIList)
from bpy.props import (StringProperty)
from ..components.operators import OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator
from .operators import(
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_OpenFilebrowser,
OT_select_component_name_to_replace,
OT_select_object, ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT)
class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_Configuration"
bl_label = "Configuration"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "list of missing/unregistered type from the bevy side"
def draw(self, context):
layout = self.layout
registry = context.window_manager.components_registry
row = layout.row()
col = row.column()
col.enabled = False
col.prop(registry, "schemaPath", text="Registry Schema path")
col = row.column()
col.operator(OT_OpenFilebrowser.bl_idname, text="Browse for registry schema file (json)")
layout.separator()
layout.operator(ReloadRegistryOperator.bl_idname, text="reload registry" , icon="FILE_REFRESH")
layout.separator()
row = layout.row()
row.prop(registry, "watcher_enabled", text="enable registry file polling")
row.prop(registry, "watcher_poll_frequency", text="registry file poll frequency (s)")
layout.separator()
layout.separator()
class BEVY_COMPONENTS_PT_AdvancedToolsPanel(bpy.types.Panel):
"""panel listing all the missing bevy types in the schema"""
bl_idname = "BEVY_COMPONENTS_PT_AdvancedToolsPanel"
bl_label = "Advanced tools"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "advanced tooling"
def draw_invalid_or_unregistered_header(self, layout, items):
row = layout.row()
for item in items:
col = row.column()
col.label(text=item)
def draw_invalid_or_unregistered(self, layout, status, component_name, object):
available_components = bpy.context.window_manager.components_list
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
row = layout.row()
col = row.column()
col.label(text=component_name)
col = row.column()
operator = col.operator(OT_select_object.bl_idname, text=object.name)
operator.object_name = object.name
col = row.column()
col.label(text=status)
col = row.column()
col.prop(available_components, "list", text="")
col = row.column()
operator = col.operator(OT_rename_component.bl_idname, text="", icon="SHADERFX") #rename
new_name = registry.type_infos[available_components.list]['long_name'] if available_components.list in registry.type_infos else ""
operator.original_name = component_name
operator.target_objects = json.dumps([object.name])
operator.new_name = new_name
col.enabled = registry_has_type_infos and component_name != "" and component_name != new_name
col = row.column()
operator = col.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
operator.object_name = object.name
operator.component_name = component_name
col = row.column()
col = row.column()
operator = col.operator(OT_select_component_name_to_replace.bl_idname, text="", icon="EYEDROPPER") #text="select for rename",
operator.component_name = component_name
def draw(self, context):
layout = self.layout
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
selected_object = context.selected_objects[0] if len(context.selected_objects) > 0 else None
available_components = bpy.context.window_manager.components_list
row = layout.row()
box= row.box()
box.label(text="Invalid/ unregistered components")
objects_with_invalid_components = []
invalid_component_names = []
self.draw_invalid_or_unregistered_header(layout, ["Component", "Object", "Status", "Target"])
for object in bpy.data.objects: # TODO: very inneficent
if len(object.keys()) > 0:
if "components_meta" in object:
components_metadata = object.components_meta.components
comp_names = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if component_meta.invalid:
self.draw_invalid_or_unregistered(layout, "Invalid", long_name, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
if not long_name in invalid_component_names:
invalid_component_names.append(long_name)
comp_names.append(long_name)
for custom_property in object.keys():
if custom_property != 'components_meta' and custom_property != 'bevy_components' and custom_property not in comp_names:
self.draw_invalid_or_unregistered(layout, "Unregistered", custom_property, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
"""if not long_name in invalid_component_names:
invalid_component_names.append(custom_property)""" # FIXME
layout.separator()
layout.separator()
original_name = bpy.context.window_manager.bevy_component_rename_helper.original_name
row = layout.row()
col = row.column()
col.label(text="Original")
col = row.column()
col.label(text="New")
col = row.column()
col.label(text="------")
row = layout.row()
col = row.column()
box = col.box()
box.label(text=original_name)
col = row.column()
col.prop(available_components, "list", text="")
#row.prop(available_components, "filter",text="Filter")
col = row.column()
components_rename_progress = context.window_manager.components_rename_progress
if components_rename_progress == -1.0:
operator = col.operator(OT_rename_component.bl_idname, text="apply", icon="SHADERFX")
operator.target_objects = json.dumps(objects_with_invalid_components)
new_name = registry.type_infos[available_components.list]['short_name'] if available_components.list in registry.type_infos else ""
operator.new_name = new_name
col.enabled = registry_has_type_infos and original_name != "" and original_name != new_name
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = components_rename_progress, text=f"updating {components_rename_progress * 100.0:.2f}%")
col = row.column()
remove_components_progress = context.window_manager.components_remove_progress
if remove_components_progress == -1.0:
operator = row.operator(RemoveComponentFromAllObjectsOperator.bl_idname, text="", icon="X")
operator.component_name = context.window_manager.bevy_component_rename_helper.original_name
col.enabled = registry_has_type_infos and original_name != ""
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = remove_components_progress, text=f"updating {remove_components_progress * 100.0:.2f}%")
layout.separator()
layout.separator()
row = layout.row()
box= row.box()
box.label(text="Conversions between custom properties and components & vice-versa")
row = layout.row()
row.label(text="WARNING ! The following operations will overwrite your existing custom properties if they have matching types on the bevy side !")
row.alert = True
##
row = layout.row()
custom_properties_from_components_progress_current = context.window_manager.custom_properties_from_components_progress
if custom_properties_from_components_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update custom properties of current object" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_current, text=f"updating {custom_properties_from_components_progress_current * 100.0:.2f}%")
layout.separator()
row = layout.row()
custom_properties_from_components_progress_all = context.window_manager.custom_properties_from_components_progress_all
if custom_properties_from_components_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL.bl_idname, text="update custom properties of ALL objects" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_all, text=f"updating {custom_properties_from_components_progress_all * 100.0:.2f}%")
########################
row = layout.row()
row.label(text="WARNING ! The following operations will try to overwrite your existing ui values if they have matching types on the bevy side !")
row.alert = True
components_from_custom_properties_progress_current = context.window_manager.components_from_custom_properties_progress
row = layout.row()
if components_from_custom_properties_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update UI FROM custom properties of current object" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_current, text=f"updating {components_from_custom_properties_progress_current * 100.0:.2f}%")
layout.separator()
row = layout.row()
components_from_custom_properties_progress_all = context.window_manager.components_from_custom_properties_progress_all
if components_from_custom_properties_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL.bl_idname, text="update UI FROM custom properties of ALL objects" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_all, text=f"updating {components_from_custom_properties_progress_all * 100.0:.2f}%")
class BEVY_COMPONENTS_PT_MissingTypesPanel(bpy.types.Panel):
"""panel listing all the missing bevy types in the schema"""
bl_idname = "BEVY_COMPONENTS_PT_MissingTypesPanel"
bl_label = "Bevy Missing/Unregistered Types"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "list of missing/unregistered type from the bevy side"
def draw(self, context):
layout = self.layout
registry = bpy.context.window_manager.components_registry
layout.label(text="Missing types ")
layout.template_list("MISSING_TYPES_UL_List", "Missing types list", registry, "missing_types_list", registry, "missing_types_list_index")
class MISSING_TYPES_UL_List(UIList):
"""Missing components UIList."""
use_filter_name_reverse: bpy.props.BoolProperty(
name="Reverse Name",
default=False,
options=set(),
description="Reverse name filtering",
) # type: ignore
use_order_name = bpy.props.BoolProperty(name="Name", default=False, options=set(),
description="Sort groups by their name (case-insensitive)")
def filter_items__(self, context, data, propname):
"""Filter and order items in the list."""
# We initialize filtered and ordered as empty lists. Notice that # if all sorting and filtering is disabled, we will return # these empty.
filtered = []
ordered = []
items = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
print("filter, order", items, self, dict(self))
if self.filter_name:
print("ssdfs", self.filter_name)
filtered= helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, items, "long_name", reverse=self.use_filter_name_reverse)
if not filtered:
filtered = [self.bitflag_filter_item] * len(items)
if self.use_order_name:
ordered = helper_funcs.sort_items_by_name(items, "name")
return filtered, ordered
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row()
#row.enabled = False
#row.alert = True
row.prop(item, "long_name", text="")
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
row = layout.row()
row.prop(item, "long_name", text="")

View File

@ -1,217 +0,0 @@
import random
import string
import uuid
from bpy_types import PropertyGroup
def random_bool():
return bool(random.getrandbits(1))
def rand_int():
return random.randint(0, 100)
def rand_float():
return random.random()
def random_word(length):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length))
def random_vec(length, type,):
value = []
for i in range(0, length):
if type == 'float':
value.append(rand_float())
if type == 'int':
value.append(rand_int())
return value
type_mappings = {
"bool": random_bool,
"u8": rand_int,
"u16": rand_int,
"u32": rand_int,
"u64": rand_int,
"u128": rand_int,
"u64": rand_int,
"usize": rand_int,
"i8": rand_int,
"i16": rand_int,
"i32": rand_int,
"i64": rand_int,
"i128": rand_int,
"isize": rand_int,
'f32': rand_float,
'f64': rand_float,
"glam::Vec2": lambda : random_vec(2, 'float'),
"glam::DVec2": lambda : random_vec(2, 'float'),
"glam::UVec2": lambda : random_vec(2, 'int'),
'glam::Vec3': lambda : random_vec(3, 'float'),
"glam::Vec3A": lambda : random_vec(3, 'float'),
"glam::UVec3": lambda : random_vec(3, 'int'),
"glam::Vec4": lambda : random_vec(4, 'float'),
"glam::DVec4": lambda : random_vec(4, 'float'),
"glam::UVec4": lambda : random_vec(4, 'int'),
"glam::Quat": lambda : random_vec(4, 'float'),
'bevy_render::color::Color': lambda : random_vec(4, 'float'),
'alloc::string::String': lambda : random_word(8),
'alloc::borrow::Cow<str>': lambda : random_word(8),
'bevy_ecs::entity::Entity': lambda: 0, #4294967295, #
'bevy_utils::Uuid': lambda: '"'+str( uuid.UUID("73b3b118-7d01-4778-8bcc-4e79055f5d22") )+'"'
}
#
def is_def_value_type(definition, registry):
if definition == None:
return True
value_types_defaults = registry.value_types_defaults
long_name = definition["long_name"]
is_value_type = long_name in value_types_defaults
return is_value_type
# see https://docs.python.org/3/library/random.html
def component_values_shuffler(seed=1, property_group=None, definition=None, registry=None, parent=None):
if parent == None:
random.seed(seed)
value_types_defaults = registry.value_types_defaults
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
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"
long_name = definition["long_name"]
#is_value_type = type_def in value_types_defaults or long_name in value_types_defaults
is_value_type = long_name in value_types_defaults
if is_value_type:
fieldValue = type_mappings[long_name]()
return fieldValue
elif type_info == "Struct":
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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 = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
#print("setting attr", field_name , "for", component_name, "to", value, "value type", is_item_value_type)
setattr(property_group , field_name, value)
elif type_info == "Tuple":
#print("tup")
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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 = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
#print("setting attr", field_name , "for", component_name, "to", value, "value type", is_item_value_type)
setattr(property_group , field_name, value)
elif type_info == "TupleStruct":
#print("tupstruct")
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_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 = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
setattr(property_group , field_name, value)
elif type_info == "Enum":
available_variants = definition["oneOf"] if type_def != "object" else list(map(lambda x: x["long_name"], definition["oneOf"]))
selected = random.choice(available_variants)
# set selected variant
setattr(property_group , "selection", selected)
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 = component_values_shuffler(seed, child_property_group, variant_definition, registry, parent=component_name)
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 = component_values_shuffler(seed, child_property_group, variant_definition, registry, parent=component_name)
value = selected + str(value,)
else:
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_list.clear()
item_long_name = getattr(property_group, "long_name")
number_of_list_items_to_add = random.randint(1, 2)
for i in range(0, number_of_list_items_to_add):
new_entry = item_list.add()
item_long_name = getattr(new_entry, "long_name") # we get the REAL type name
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
component_values_shuffler(seed, new_entry, definition, registry, parent=component_name)
else:
pass
else:
print("something else")
fieldValue = type_mappings[long_name]() if long_name in type_mappings else 'None'
return fieldValue
#return value

View File

@ -1,555 +0,0 @@
expected_custom_property_values = {'bevy_animation::AnimationPlayer': '(animation: "", paused: true)',
'bevy_asset::handle::Handle<()>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_animation::AnimationClip>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset::assets::LoadedUntypedAsset>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset::folder::LoadedFolder>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset_loader::standard_dynamic_asset::StandardDynamicAssetCollection>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_audio::audio_source::AudioSource>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_audio::pitch::Pitch>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gizmos::LineGizmo>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::Gltf>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfMesh>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfNode>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfPrimitive>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::extended_material::ExtendedMaterial<bevy_pbr::pbr_material::StandardMaterial, bevy_example::test_components::MyExtension>>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::pbr_material::StandardMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::wireframe::WireframeMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::mesh::mesh::Mesh>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::mesh::mesh::skinning::SkinnedMeshInverseBindposes>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::render_resource::shader::Shader>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::texture::image::Image>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_scene::dynamic_scene::DynamicScene>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_scene::scene::Scene>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_sprite::mesh2d::color_material::ColorMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_sprite::texture_atlas::TextureAtlasLayout>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_text::font::Font>': 'Strong("")',
'bevy_audio::audio::PlaybackSettings': '(mode: Once, paused: true, spatial: true, spatial_scale: "", speed: 0.0, '
'volume: (0.0))',
'bevy_audio::audio::SpatialListener': '(left_ear_offset: Vec3(x:0.0, y:0.0, z:0.0), right_ear_offset: Vec3(x:0.0, '
'y:0.0, z:0.0))',
'bevy_core::name::Name': '(hash: 0, name: " ")',
'bevy_core_pipeline::bloom::settings::BloomSettings': '(composite_mode: EnergyConserving, high_pass_frequency: 0.0, '
'intensity: 0.0, low_frequency_boost: 0.0, '
'low_frequency_boost_curvature: 0.0, prefilter_settings: '
'(threshold: 0.0, threshold_softness: 0.0))',
'bevy_core_pipeline::contrast_adaptive_sharpening::ContrastAdaptiveSharpeningSettings': '(denoise: true, enabled: '
'true, sharpening_strength: '
'0.0)',
'bevy_core_pipeline::core_2d::camera_2d::Camera2d': '()',
'bevy_core_pipeline::core_3d::camera_3d::Camera3d': '(depth_load_op: Clear(0.0), depth_texture_usages: (0), '
'screen_space_specular_transmission_quality: Low, '
'screen_space_specular_transmission_steps: 0)',
'bevy_core_pipeline::fxaa::Fxaa': '(edge_threshold: "", edge_threshold_min: "", enabled: true)',
'bevy_core_pipeline::tonemapping::DebandDither': 'Disabled',
'bevy_core_pipeline::tonemapping::Tonemapping': 'None',
'bevy_example::dupe_components::EnumTest': 'Metal',
'bevy_example::game::animation::Marker1': '()',
'bevy_example::game::animation::Marker2': '()',
'bevy_example::game::animation::Marker3': '()',
'bevy_example::game::animation::MarkerFox': '()',
'bevy_example::test_components::AComponentWithAnExtremlyExageratedOrMaybeNotButCouldBeNameOrWut': '()',
'bevy_example::test_components::BasicTest': '(a: 0.0, b: 0, c: " ")',
'bevy_example::test_components::EnumComplex': 'Float(0.0)',
'bevy_example::test_components::EnumTest': 'Metal',
'bevy_example::test_components::HashmapTestIntColor': '(inner: {})',
'bevy_example::test_components::HashmapTestIntString': '(named_animations: {})',
'bevy_example::test_components::HashmapTestSimple': '(named_animations: {})',
'bevy_example::test_components::HashmapTestStringColor': '(inner: {})',
'bevy_example::test_components::HashmapTestStringColorFlat': '({})',
'bevy_example::test_components::HashmapTestStringFloat': '(named_animations: {})',
'bevy_example::test_components::NestedTupleStuff': '(0.0, 0, (basic: (a: 0.0, b: 0, c: " "), color: (Rgba(red:1.0, '
'green:1.0, blue:0.0, alpha:1.0)), colors_list: ([]), enable: '
'true, enum_inner: Metal, nested: (vec: (Vec3(x:0.0, y:0.0, '
'z:0.0))), text: " ", toggle: (true)))',
'bevy_example::test_components::NestingTestLevel2': '(basic: (a: 0.0, b: 0, c: " "), color: (Rgba(red:1.0, green:1.0, '
'blue:0.0, alpha:1.0)), colors_list: ([]), enable: true, '
'enum_inner: Metal, nested: (vec: (Vec3(x:0.0, y:0.0, z:0.0))), '
'text: " ", toggle: (true))',
'bevy_example::test_components::NestingTestLevel3': '(vec: (Vec3(x:0.0, y:0.0, z:0.0)))',
'bevy_example::test_components::TupleTest2': '(0.0, 0, " ")',
'bevy_example::test_components::TupleTestBool': '(true)',
'bevy_example::test_components::TupleTestColor': '(Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))',
'bevy_example::test_components::TupleTestF32': '(0.0)',
'bevy_example::test_components::TupleTestStr': '(" ")',
'bevy_example::test_components::TupleTestU64': '(0)',
'bevy_example::test_components::TupleVec': '([])',
'bevy_example::test_components::TupleVec2': '(Vec2(x:0.0, y:0.0))',
'bevy_example::test_components::TupleVec3': '(Vec3(x:0.0, y:0.0, z:0.0))',
'bevy_example::test_components::TupleVecF32F32': '([])',
'bevy_example::test_components::UnitTest': '()',
'bevy_example::test_components::VecOfColors': '([])',
'bevy_example::test_components::VecOfF32s': '([])',
'bevy_example::test_components::VecOfVec3s2': '([])',
'bevy_gltf::GltfExtras': '(value: " ")',
'bevy_gltf_blueprints::animation::AnimationInfos': '(animations: [])',
'bevy_gltf_blueprints::animation::AnimationMarkers': '({})',
'bevy_gltf_blueprints::animation::BlueprintAnimations': '(named_animations: "")',
'bevy_gltf_blueprints::animation::SceneAnimations': '(named_animations: "")',
'bevy_gltf_blueprints::materials::MaterialInfo': '(name: " ", source: " ")',
'bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName': '(" ")',
'bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList': '({})',
'bevy_gltf_blueprints::spawn_from_blueprints::SpawnHere': '()',
'bevy_gltf_components::GltfProcessed': '()',
'bevy_gltf_components::blender_settings::lighting::BlenderBackgroundShader': '(color: Rgba(red:1.0, green:1.0, '
'blue:0.0, alpha:1.0), strength: 0.0)',
'bevy_gltf_components::blender_settings::lighting::BlenderLightShadows': '(buffer_bias: 0.0, enabled: true)',
'bevy_gltf_components::blender_settings::lighting::BlenderShadowSettings': '(cascade_size: 0)',
'bevy_gltf_worlflow_examples_common::core::camera::camera_replace_proxies::SSAOSettings': '()',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTrackable': '()',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTracking': '(offset: Vec3(x:0.0, y:0.0, '
'z:0.0))',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTrackingOffset': '(Vec3(x:0.0, y:0.0, '
'z:0.0))',
'bevy_gltf_worlflow_examples_common::game::picking::Pickable': '()',
'bevy_gltf_worlflow_examples_common::game::player::Player': '()',
'bevy_gltf_worlflow_examples_common_rapier::physics::physics_replace_proxies::AutoAABBCollider': 'Cuboid',
'bevy_gltf_worlflow_examples_common_rapier::physics::physics_replace_proxies::Collider': 'Ball(0.0)',
'bevy_hierarchy::components::children::Children': '([])',
'bevy_hierarchy::components::parent::Parent': '(0)',
'bevy_pbr::bundle::CascadesVisibleEntities': '()',
'bevy_pbr::bundle::CubemapVisibleEntities': '()',
'bevy_pbr::fog::FogSettings': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), directional_light_color: '
'Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), directional_light_exponent: 0.0, '
'falloff: Linear(end: 0.0, start: 0.0))',
'bevy_pbr::light::CascadeShadowConfig': '(bounds: [], minimum_distance: 0.0, overlap_proportion: 0.0)',
'bevy_pbr::light::Cascades': '(cascades: "")',
'bevy_pbr::light::ClusterConfig': 'None',
'bevy_pbr::light::DirectionalLight': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), illuminance: 0.0, '
'shadow_depth_bias: 0.0, shadow_normal_bias: 0.0, shadows_enabled: true)',
'bevy_pbr::light::NotShadowCaster': '()',
'bevy_pbr::light::NotShadowReceiver': '()',
'bevy_pbr::light::PointLight': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), intensity: 0.0, radius: 0.0, '
'range: 0.0, shadow_depth_bias: 0.0, shadow_normal_bias: 0.0, shadows_enabled: true)',
'bevy_pbr::light::ShadowFilteringMethod': 'Hardware2x2',
'bevy_pbr::light::SpotLight': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), inner_angle: 0.0, intensity: '
'0.0, outer_angle: 0.0, radius: 0.0, range: 0.0, shadow_depth_bias: 0.0, '
'shadow_normal_bias: 0.0, shadows_enabled: true)',
'bevy_pbr::light_probe::LightProbe': '()',
'bevy_pbr::ssao::ScreenSpaceAmbientOcclusionSettings': '(quality_level: "")',
'bevy_pbr::wireframe::NoWireframe': '()',
'bevy_pbr::wireframe::Wireframe': '()',
'bevy_pbr::wireframe::WireframeColor': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))',
'bevy_rapier3d::dynamics::rigid_body::AdditionalMassProperties': 'Mass(0.0)',
'bevy_rapier3d::dynamics::rigid_body::Ccd': '(enabled: true)',
'bevy_rapier3d::dynamics::rigid_body::Damping': '(angular_damping: 0.0, linear_damping: 0.0)',
'bevy_rapier3d::dynamics::rigid_body::Dominance': '(groups: 0)',
'bevy_rapier3d::dynamics::rigid_body::ExternalForce': '(force: Vec3(x:0.0, y:0.0, z:0.0), torque: Vec3(x:0.0, y:0.0, '
'z:0.0))',
'bevy_rapier3d::dynamics::rigid_body::ExternalImpulse': '(impulse: Vec3(x:0.0, y:0.0, z:0.0), torque_impulse: '
'Vec3(x:0.0, y:0.0, z:0.0))',
'bevy_rapier3d::dynamics::rigid_body::GravityScale': '(0.0)',
'bevy_rapier3d::dynamics::rigid_body::LockedAxes': '(0)',
'bevy_rapier3d::dynamics::rigid_body::RigidBody': 'Dynamic',
'bevy_rapier3d::dynamics::rigid_body::Sleeping': '(angular_threshold: 0.0, linear_threshold: 0.0, sleeping: true)',
'bevy_rapier3d::dynamics::rigid_body::Velocity': '(angvel: Vec3(x:0.0, y:0.0, z:0.0), linvel: Vec3(x:0.0, y:0.0, '
'z:0.0))',
'bevy_rapier3d::geometry::collider::CollidingEntities': '("")',
'bevy_rapier3d::geometry::collider::CollisionGroups': '(filters: (0), memberships: (0))',
'bevy_rapier3d::geometry::collider::ContactForceEventThreshold': '(0.0)',
'bevy_rapier3d::geometry::collider::Friction': '(coefficient: 0.0, combine_rule: "")',
'bevy_rapier3d::geometry::collider::Group': '(0)',
'bevy_rapier3d::geometry::collider::Restitution': '(coefficient: 0.0, combine_rule: "")',
'bevy_rapier3d::geometry::collider::Sensor': '()',
'bevy_rapier3d::geometry::collider::SolverGroups': '(filters: (0), memberships: (0))',
'bevy_render::camera::camera::Camera': '(clear_color: Default, hdr: true, is_active: true, msaa_writeback: true, '
'order: 0, viewport: None)',
'bevy_render::camera::camera::CameraMainTextureUsages': 'None',
'bevy_render::camera::camera::CameraRenderGraph': 'None',
'bevy_render::camera::camera::Exposure': 'None',
'bevy_render::camera::projection::OrthographicProjection': '(area: (max: Vec2(x:0.0, y:0.0), min: Vec2(x:0.0, '
'y:0.0)), far: 0.0, near: 0.0, scale: 0.0, scaling_mode: '
'Fixed(height: 0.0, width: 0.0), viewport_origin: '
'Vec2(x:0.0, y:0.0))',
'bevy_render::camera::projection::PerspectiveProjection': '(aspect_ratio: 0.0, far: 0.0, fov: 0.0, near: 0.0)',
'bevy_render::camera::projection::Projection': 'Perspective((aspect_ratio: 0.0, far: 0.0, fov: 0.0, near: 0.0))',
'bevy_render::mesh::mesh::skinning::SkinnedMesh': '(inverse_bindposes: Strong(""), joints: [])',
'bevy_render::mesh::morph::MeshMorphWeights': '(weights: [])',
'bevy_render::mesh::morph::MorphWeights': '(first_mesh: "", weights: [])',
'bevy_render::primitives::Aabb': '(center: Vec3A(x:0.0, y:0.0, z:0.0), half_extents: Vec3A(x:0.0, y:0.0, z:0.0))',
'bevy_render::primitives::CascadesFrusta': '()',
'bevy_render::primitives::CubemapFrusta': '()',
'bevy_render::primitives::Frustum': '()',
'bevy_render::view::ColorGrading': '(exposure: 0.0, gamma: 0.0, post_saturation: 0.0, pre_saturation: 0.0)',
'bevy_render::view::visibility::InheritedVisibility': '(true)',
'bevy_render::view::visibility::NoFrustumCulling': '()',
'bevy_render::view::visibility::ViewVisibility': '(true)',
'bevy_render::view::visibility::Visibility': 'Inherited',
'bevy_render::view::visibility::VisibleEntities': '()',
'bevy_render::view::visibility::render_layers::RenderLayers': '(0)',
'bevy_sprite::mesh2d::mesh::Mesh2dHandle': '(Strong(""))',
'bevy_sprite::sprite::ImageScaleMode': 'Sliced((border: "", center_scale_mode: "", max_corner_scale: 0.0, '
'sides_scale_mode: ""))',
'bevy_sprite::sprite::Sprite': '(anchor: Center, color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), custom_size: '
'"", flip_x: true, flip_y: true, rect: "")',
'bevy_text::pipeline::TextLayoutInfo': '(glyphs: "", logical_size: Vec2(x:0.0, y:0.0))',
'bevy_text::text2d::Text2dBounds': '(size: Vec2(x:0.0, y:0.0))',
'bevy_text::text::Text': '(justify: Left, linebreak_behavior: WordBoundary, sections: [])',
'bevy_transform::components::global_transform::GlobalTransform': '((matrix3: (x_axis: Vec3A(x:0.0, y:0.0, z:0.0), '
'y_axis: Vec3A(x:0.0, y:0.0, z:0.0), z_axis: '
'Vec3A(x:0.0, y:0.0, z:0.0)), translation: '
'Vec3A(x:0.0, y:0.0, z:0.0)))',
'bevy_transform::components::transform::Transform': '(rotation: Quat(x:0.0, y:0.0, z:0.0, w:0.0), scale: Vec3(x:0.0, '
'y:0.0, z:0.0), translation: Vec3(x:0.0, y:0.0, z:0.0))',
'bevy_ui::focus::FocusPolicy': 'Block',
'bevy_ui::focus::Interaction': 'Pressed',
'bevy_ui::focus::RelativeCursorPosition': '(normalized: "", normalized_visible_node_rect: (max: Vec2(x:0.0, y:0.0), '
'min: Vec2(x:0.0, y:0.0)))',
'bevy_ui::measurement::ContentSize': '()',
'bevy_ui::ui_node::BackgroundColor': '(Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))',
'bevy_ui::ui_node::BorderColor': '(Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0))',
'bevy_ui::ui_node::CalculatedClip': '(clip: (max: Vec2(x:0.0, y:0.0), min: Vec2(x:0.0, y:0.0)))',
'bevy_ui::ui_node::Node': '(calculated_size: Vec2(x:0.0, y:0.0), outline_offset: 0.0, outline_width: 0.0, '
'stack_index: 0, unrounded_size: Vec2(x:0.0, y:0.0))',
'bevy_ui::ui_node::Outline': '(color: Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), offset: Auto, width: Auto)',
'bevy_ui::ui_node::Style': '(align_content: Default, align_items: Default, align_self: Auto, aspect_ratio: None, '
'border: (bottom: Auto, left: Auto, right: Auto, top: Auto), bottom: Auto, column_gap: '
'Auto, direction: Inherit, display: Flex, flex_basis: Auto, flex_direction: Row, '
'flex_grow: 0.0, flex_shrink: 0.0, flex_wrap: NoWrap, grid_auto_columns: "", '
'grid_auto_flow: Row, grid_auto_rows: "", grid_column: (end: "", span: "", start: ""), '
'grid_row: (end: "", span: "", start: ""), grid_template_columns: "", grid_template_rows: '
'"", height: Auto, justify_content: Default, justify_items: Default, justify_self: Auto, '
'left: Auto, margin: (bottom: Auto, left: Auto, right: Auto, top: Auto), max_height: Auto, '
'max_width: Auto, min_height: Auto, min_width: Auto, overflow: (x: Visible, y: Visible), '
'padding: (bottom: Auto, left: Auto, right: Auto, top: Auto), position_type: Relative, '
'right: Auto, row_gap: Auto, top: Auto, width: Auto)',
'bevy_ui::ui_node::UiImage': '(flip_x: true, flip_y: true, texture: Strong(""))',
'bevy_ui::ui_node::ZIndex': 'Local(0)',
'bevy_ui::widget::button::Button': '()',
'bevy_ui::widget::image::UiImageSize': '(size: Vec2(x:0.0, y:0.0))',
'bevy_ui::widget::label::Label': '()',
'bevy_ui::widget::text::TextFlags': '(needs_new_measure_func: true, needs_recompute: true)',
'bevy_window::window::PrimaryWindow': '()',
'bevy_window::window::Window': '(canvas: None, composite_alpha_mode: Auto, cursor: (grab_mode: None, hit_test: true, '
'icon: Default, visible: true), decorations: true, enabled_buttons: (close: true, '
'maximize: true, minimize: true), focused: true, ime_enabled: true, ime_position: '
'Vec2(x:0.0, y:0.0), internal: (maximize_request: None, minimize_request: None, '
'physical_cursor_position: None), mode: Windowed, name: None, position: Automatic, '
'present_mode: AutoVsync, prevent_default_event_handling: true, resizable: true, '
'resize_constraints: (max_height: 0.0, max_width: 0.0, min_height: 0.0, min_width: '
'0.0), resolution: (physical_height: 0, physical_width: 0, scale_factor: 0.0, '
'scale_factor_override: None), title: " ", transparent: true, visible: true, '
'window_level: AlwaysOnBottom, window_theme: "")'}
expected_custom_property_values_randomized = {'bevy_animation::AnimationPlayer': '(animation: "", paused: true)',
'bevy_asset::handle::Handle<()>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_animation::AnimationClip>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset::assets::LoadedUntypedAsset>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset::folder::LoadedFolder>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_asset_loader::standard_dynamic_asset::StandardDynamicAssetCollection>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_audio::audio_source::AudioSource>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_audio::pitch::Pitch>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gizmos::LineGizmo>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::Gltf>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfMesh>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfNode>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_gltf::GltfPrimitive>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::extended_material::ExtendedMaterial<bevy_pbr::pbr_material::StandardMaterial, bevy_example::test_components::MyExtension>>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::pbr_material::StandardMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_pbr::wireframe::WireframeMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::mesh::mesh::Mesh>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::mesh::mesh::skinning::SkinnedMeshInverseBindposes>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::render_resource::shader::Shader>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_render::texture::image::Image>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_scene::dynamic_scene::DynamicScene>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_scene::scene::Scene>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_sprite::mesh2d::color_material::ColorMaterial>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_sprite::texture_atlas::TextureAtlasLayout>': 'Strong("")',
'bevy_asset::handle::Handle<bevy_text::font::Font>': 'Strong("")',
'bevy_audio::audio::PlaybackSettings': '(mode: Once, paused: false, spatial: false, spatial_scale: "", speed: '
'0.5780913233757019, volume: (0.20609822869300842))',
'bevy_audio::audio::SpatialListener': '(left_ear_offset: Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019), right_ear_offset: Vec3(x:0.20609822869300842, '
'y:0.8133212327957153, z:0.8235888481140137))',
'bevy_core::name::Name': '(hash: 73, name: "bnpsagop")',
'bevy_core_pipeline::bloom::settings::BloomSettings': '(composite_mode: EnergyConserving, high_pass_frequency: '
'0.42888906598091125, intensity: 0.5780913233757019, '
'low_frequency_boost: 0.20609822869300842, '
'low_frequency_boost_curvature: 0.8133212327957153, '
'prefilter_settings: (threshold: 0.8235888481140137, '
'threshold_softness: 0.6534725427627563))',
'bevy_core_pipeline::contrast_adaptive_sharpening::ContrastAdaptiveSharpeningSettings': '(denoise: true, enabled: '
'false, sharpening_strength: '
'0.42888906598091125)',
'bevy_core_pipeline::core_2d::camera_2d::Camera2d': '()',
'bevy_core_pipeline::core_3d::camera_3d::Camera3d': '(depth_load_op: Clear(0.42888906598091125), '
'depth_texture_usages: (73), '
'screen_space_specular_transmission_quality: Low, '
'screen_space_specular_transmission_steps: 26)',
'bevy_core_pipeline::fxaa::Fxaa': '(edge_threshold: "", edge_threshold_min: "", enabled: true)',
'bevy_core_pipeline::tonemapping::DebandDither': 'Disabled',
'bevy_core_pipeline::tonemapping::Tonemapping': 'None',
'bevy_example::dupe_components::EnumTest': 'Squishy',
'bevy_example::game::animation::Marker1': '()',
'bevy_example::game::animation::Marker2': '()',
'bevy_example::game::animation::Marker3': '()',
'bevy_example::game::animation::MarkerFox': '()',
'bevy_example::test_components::AComponentWithAnExtremlyExageratedOrMaybeNotButCouldBeNameOrWut': '()',
'bevy_example::test_components::BasicTest': '(a: 0.5714026093482971, b: 54, c: "psagopiu")',
'bevy_example::test_components::EnumComplex': 'StructLike(a: 0.03258506581187248, b: 61, c: "sagopiuz")',
'bevy_example::test_components::EnumTest': 'Squishy',
'bevy_example::test_components::HashmapTestIntColor': '(inner: {})',
'bevy_example::test_components::HashmapTestIntString': '(named_animations: {})',
'bevy_example::test_components::HashmapTestSimple': '(named_animations: {})',
'bevy_example::test_components::HashmapTestStringColor': '(inner: {})',
'bevy_example::test_components::HashmapTestStringColorFlat': '({})',
'bevy_example::test_components::HashmapTestStringFloat': '(named_animations: {})',
'bevy_example::test_components::NestedTupleStuff': '(0.5714026093482971, 54, (basic: (a: 0.4825616776943207, b: 1, c: '
'"gopiuzfb"), color: (Rgba(red:0.5206693410873413, '
'green:0.3277728259563446, blue:0.24999667704105377, '
'alpha:0.952816903591156)), colors_list: '
'([Rgba(red:0.0445563830435276, green:0.8601610660552979, '
'blue:0.6031906008720398, alpha:0.38160598278045654), '
'Rgba(red:0.2836182117462158, green:0.6749648451805115, '
'blue:0.456831157207489, alpha:0.6858614683151245)]), enable: '
'true, enum_inner: Rock, nested: (vec: (Vec3(x:0.1329781413078308, '
'y:0.7678378224372864, z:0.9824132323265076))), text: "otmbsahe", '
'toggle: (false)))',
'bevy_example::test_components::NestingTestLevel2': '(basic: (a: 0.5714026093482971, b: 54, c: "psagopiu"), color: '
'(Rgba(red:0.8106188178062439, green:0.03440357372164726, '
'blue:0.49008557200431824, alpha:0.07608934491872787)), '
'colors_list: ([Rgba(red:0.0445563830435276, '
'green:0.8601610660552979, blue:0.6031906008720398, '
'alpha:0.38160598278045654), Rgba(red:0.2836182117462158, '
'green:0.6749648451805115, blue:0.456831157207489, '
'alpha:0.6858614683151245)]), enable: true, enum_inner: Rock, '
'nested: (vec: (Vec3(x:0.1329781413078308, y:0.7678378224372864, '
'z:0.9824132323265076))), text: "otmbsahe", toggle: (false))',
'bevy_example::test_components::NestingTestLevel3': '(vec: (Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019)))',
'bevy_example::test_components::TupleTest2': '(0.5714026093482971, 54, "psagopiu")',
'bevy_example::test_components::TupleTestBool': '(true)',
'bevy_example::test_components::TupleTestColor': '(Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842))',
'bevy_example::test_components::TupleTestF32': '(0.5714026093482971)',
'bevy_example::test_components::TupleTestStr': '("sbnpsago")',
'bevy_example::test_components::TupleTestU64': '(73)',
'bevy_example::test_components::TupleVec': '(["npsagopi"])',
'bevy_example::test_components::TupleVec2': '(Vec2(x:0.5714026093482971, y:0.42888906598091125))',
'bevy_example::test_components::TupleVec3': '(Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019))',
'bevy_example::test_components::TupleVecF32F32': '([(0.42888906598091125, 0.5780913233757019)])',
'bevy_example::test_components::UnitTest': '()',
'bevy_example::test_components::VecOfColors': '([Rgba(red:0.42888906598091125, green:0.5780913233757019, '
'blue:0.20609822869300842, alpha:0.8133212327957153)])',
'bevy_example::test_components::VecOfF32s': '([0.42888906598091125])',
'bevy_example::test_components::VecOfVec3s2': '([(Vec3(x:0.42888906598091125, y:0.5780913233757019, '
'z:0.20609822869300842))])',
'bevy_gltf::GltfExtras': '(value: "sbnpsago")',
'bevy_gltf_blueprints::animation::AnimationInfos': '(animations: [(frame_end: 0.42888906598091125, '
'frame_end_override: 0.5780913233757019, frame_start: '
'0.20609822869300842, frame_start_override: 0.8133212327957153, '
'frames_length: 0.8235888481140137, name: "uzfbqpkc")])',
'bevy_gltf_blueprints::animation::AnimationMarkers': '({})',
'bevy_gltf_blueprints::animation::BlueprintAnimations': '(named_animations: "")',
'bevy_gltf_blueprints::animation::SceneAnimations': '(named_animations: "")',
'bevy_gltf_blueprints::materials::MaterialInfo': '(name: "sbnpsago", source: "piuzfbqp")',
'bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName': '("sbnpsago")',
'bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList': '({})',
'bevy_gltf_blueprints::spawn_from_blueprints::SpawnHere': '()',
'bevy_gltf_components::GltfProcessed': '()',
'bevy_gltf_components::blender_settings::lighting::BlenderBackgroundShader': '(color: Rgba(red:0.5714026093482971, '
'green:0.42888906598091125, '
'blue:0.5780913233757019, '
'alpha:0.20609822869300842), strength: '
'0.8133212327957153)',
'bevy_gltf_components::blender_settings::lighting::BlenderLightShadows': '(buffer_bias: 0.5714026093482971, enabled: '
'false)',
'bevy_gltf_components::blender_settings::lighting::BlenderShadowSettings': '(cascade_size: 73)',
'bevy_gltf_worlflow_examples_common::core::camera::camera_replace_proxies::SSAOSettings': '()',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTrackable': '()',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTracking': '(offset: '
'Vec3(x:0.5714026093482971, '
'y:0.42888906598091125, '
'z:0.5780913233757019))',
'bevy_gltf_worlflow_examples_common::core::camera::camera_tracking::CameraTrackingOffset': '(Vec3(x:0.5714026093482971, '
'y:0.42888906598091125, '
'z:0.5780913233757019))',
'bevy_gltf_worlflow_examples_common::game::picking::Pickable': '()',
'bevy_gltf_worlflow_examples_common::game::player::Player': '()',
'bevy_gltf_worlflow_examples_common_rapier::physics::physics_replace_proxies::AutoAABBCollider': 'Capsule',
'bevy_gltf_worlflow_examples_common_rapier::physics::physics_replace_proxies::Collider': 'Ball(0.42888906598091125)',
'bevy_hierarchy::components::children::Children': '([0])',
'bevy_hierarchy::components::parent::Parent': '(0)',
'bevy_pbr::bundle::CascadesVisibleEntities': '()',
'bevy_pbr::bundle::CubemapVisibleEntities': '()',
'bevy_pbr::fog::FogSettings': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842), directional_light_color: '
'Rgba(red:0.8133212327957153, green:0.8235888481140137, blue:0.6534725427627563, '
'alpha:0.16022956371307373), directional_light_exponent: 0.5206693410873413, falloff: '
'ExponentialSquared(density: 0.07608934491872787))',
'bevy_pbr::light::CascadeShadowConfig': '(bounds: [0.42888906598091125], minimum_distance: 0.5780913233757019, '
'overlap_proportion: 0.20609822869300842)',
'bevy_pbr::light::Cascades': '(cascades: "")',
'bevy_pbr::light::ClusterConfig': 'None',
'bevy_pbr::light::DirectionalLight': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842), illuminance: '
'0.8133212327957153, shadow_depth_bias: 0.8235888481140137, shadow_normal_bias: '
'0.6534725427627563, shadows_enabled: false)',
'bevy_pbr::light::NotShadowCaster': '()',
'bevy_pbr::light::NotShadowReceiver': '()',
'bevy_pbr::light::PointLight': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842), intensity: 0.8133212327957153, '
'radius: 0.8235888481140137, range: 0.6534725427627563, shadow_depth_bias: '
'0.16022956371307373, shadow_normal_bias: 0.5206693410873413, shadows_enabled: false)',
'bevy_pbr::light::ShadowFilteringMethod': 'Jimenez14',
'bevy_pbr::light::SpotLight': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842), inner_angle: 0.8133212327957153, '
'intensity: 0.8235888481140137, outer_angle: 0.6534725427627563, radius: '
'0.16022956371307373, range: 0.5206693410873413, shadow_depth_bias: 0.3277728259563446, '
'shadow_normal_bias: 0.24999667704105377, shadows_enabled: true)',
'bevy_pbr::light_probe::LightProbe': '()',
'bevy_pbr::ssao::ScreenSpaceAmbientOcclusionSettings': '(quality_level: "")',
'bevy_pbr::wireframe::NoWireframe': '()',
'bevy_pbr::wireframe::Wireframe': '()',
'bevy_pbr::wireframe::WireframeColor': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842))',
'bevy_rapier3d::dynamics::rigid_body::AdditionalMassProperties': 'Mass(0.42888906598091125)',
'bevy_rapier3d::dynamics::rigid_body::Ccd': '(enabled: true)',
'bevy_rapier3d::dynamics::rigid_body::Damping': '(angular_damping: 0.5714026093482971, linear_damping: '
'0.42888906598091125)',
'bevy_rapier3d::dynamics::rigid_body::Dominance': '(groups: 73)',
'bevy_rapier3d::dynamics::rigid_body::ExternalForce': '(force: Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019), torque: Vec3(x:0.20609822869300842, '
'y:0.8133212327957153, z:0.8235888481140137))',
'bevy_rapier3d::dynamics::rigid_body::ExternalImpulse': '(impulse: Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019), torque_impulse: '
'Vec3(x:0.20609822869300842, y:0.8133212327957153, '
'z:0.8235888481140137))',
'bevy_rapier3d::dynamics::rigid_body::GravityScale': '(0.5714026093482971)',
'bevy_rapier3d::dynamics::rigid_body::LockedAxes': '(73)',
'bevy_rapier3d::dynamics::rigid_body::RigidBody': 'Dynamic',
'bevy_rapier3d::dynamics::rigid_body::Sleeping': '(angular_threshold: 0.5714026093482971, linear_threshold: '
'0.42888906598091125, sleeping: true)',
'bevy_rapier3d::dynamics::rigid_body::Velocity': '(angvel: Vec3(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019), linvel: Vec3(x:0.20609822869300842, '
'y:0.8133212327957153, z:0.8235888481140137))',
'bevy_rapier3d::geometry::collider::CollidingEntities': '("")',
'bevy_rapier3d::geometry::collider::CollisionGroups': '(filters: (73), memberships: (4))',
'bevy_rapier3d::geometry::collider::ContactForceEventThreshold': '(0.5714026093482971)',
'bevy_rapier3d::geometry::collider::Friction': '(coefficient: 0.5714026093482971, combine_rule: "")',
'bevy_rapier3d::geometry::collider::Group': '(73)',
'bevy_rapier3d::geometry::collider::Restitution': '(coefficient: 0.5714026093482971, combine_rule: "")',
'bevy_rapier3d::geometry::collider::Sensor': '()',
'bevy_rapier3d::geometry::collider::SolverGroups': '(filters: (73), memberships: (4))',
'bevy_render::camera::camera::Camera': '(clear_color: None, hdr: false, is_active: false, msaa_writeback: false, '
'order: 73, viewport: None)',
'bevy_render::camera::camera::CameraMainTextureUsages': 'None',
'bevy_render::camera::camera::CameraRenderGraph': 'None',
'bevy_render::camera::camera::Exposure': 'None',
'bevy_render::camera::projection::OrthographicProjection': '(area: (max: Vec2(x:0.5714026093482971, '
'y:0.42888906598091125), min: Vec2(x:0.5780913233757019, '
'y:0.20609822869300842)), far: 0.8133212327957153, near: '
'0.8235888481140137, scale: 0.6534725427627563, '
'scaling_mode: WindowSize(0.03440357372164726), '
'viewport_origin: Vec2(x:0.49008557200431824, '
'y:0.07608934491872787))',
'bevy_render::camera::projection::PerspectiveProjection': '(aspect_ratio: 0.5714026093482971, far: '
'0.42888906598091125, fov: 0.5780913233757019, near: '
'0.20609822869300842)',
'bevy_render::camera::projection::Projection': 'Perspective((aspect_ratio: 0.42888906598091125, far: '
'0.5780913233757019, fov: 0.20609822869300842, near: '
'0.8133212327957153))',
'bevy_render::mesh::mesh::skinning::SkinnedMesh': '(inverse_bindposes: Strong(""), joints: [0, 0])',
'bevy_render::mesh::morph::MeshMorphWeights': '(weights: [0.42888906598091125])',
'bevy_render::mesh::morph::MorphWeights': '(first_mesh: "", weights: [0.42888906598091125])',
'bevy_render::primitives::Aabb': '(center: Vec3A(x:0.5714026093482971, y:0.42888906598091125, z:0.5780913233757019), '
'half_extents: Vec3A(x:0.20609822869300842, y:0.8133212327957153, '
'z:0.8235888481140137))',
'bevy_render::primitives::CascadesFrusta': '()',
'bevy_render::primitives::CubemapFrusta': '()',
'bevy_render::primitives::Frustum': '()',
'bevy_render::view::ColorGrading': '(exposure: 0.5714026093482971, gamma: 0.42888906598091125, post_saturation: '
'0.5780913233757019, pre_saturation: 0.20609822869300842)',
'bevy_render::view::visibility::InheritedVisibility': '(true)',
'bevy_render::view::visibility::NoFrustumCulling': '()',
'bevy_render::view::visibility::ViewVisibility': '(true)',
'bevy_render::view::visibility::Visibility': 'Visible',
'bevy_render::view::visibility::VisibleEntities': '()',
'bevy_render::view::visibility::render_layers::RenderLayers': '(73)',
'bevy_sprite::mesh2d::mesh::Mesh2dHandle': '(Strong(""))',
'bevy_sprite::sprite::ImageScaleMode': 'Sliced((border: "", center_scale_mode: "", max_corner_scale: '
'0.42888906598091125, sides_scale_mode: ""))',
'bevy_sprite::sprite::Sprite': '(anchor: Custom(Vec2(x:0.03258506581187248, y:0.4825616776943207)), color: '
'Rgba(red:0.014832446351647377, green:0.46258050203323364, blue:0.4912964105606079, '
'alpha:0.27752065658569336), custom_size: "", flip_x: true, flip_y: false, rect: "")',
'bevy_text::pipeline::TextLayoutInfo': '(glyphs: "", logical_size: Vec2(x:0.5714026093482971, y:0.42888906598091125))',
'bevy_text::text2d::Text2dBounds': '(size: Vec2(x:0.5714026093482971, y:0.42888906598091125))',
'bevy_text::text::Text': '(justify: Right, linebreak_behavior: WordBoundary, sections: [(style: (color: '
'Rgba(red:0.4825616776943207, green:0.014832446351647377, blue:0.46258050203323364, '
'alpha:0.4912964105606079), font: Weak(Index(index: "")), font_size: 0.03440357372164726), '
'value: "pkchxlbn"), (style: (color: Rgba(red:0.8601610660552979, green:0.6031906008720398, '
'blue:0.38160598278045654, alpha:0.2836182117462158), font: Weak(Uuid(uuid: '
'"73b3b118-7d01-4778-8bcc-4e79055f5d22")), font_size: 0.17467059195041656), value: '
'"jvleoyho")])',
'bevy_transform::components::global_transform::GlobalTransform': '((matrix3: (x_axis: Vec3A(x:0.5714026093482971, '
'y:0.42888906598091125, z:0.5780913233757019), '
'y_axis: Vec3A(x:0.20609822869300842, '
'y:0.8133212327957153, z:0.8235888481140137), '
'z_axis: Vec3A(x:0.6534725427627563, '
'y:0.16022956371307373, z:0.5206693410873413)), '
'translation: Vec3A(x:0.3277728259563446, '
'y:0.24999667704105377, z:0.952816903591156)))',
'bevy_transform::components::transform::Transform': '(rotation: Quat(x:0.5714026093482971, y:0.42888906598091125, '
'z:0.5780913233757019, w:0.20609822869300842), scale: '
'Vec3(x:0.8133212327957153, y:0.8235888481140137, '
'z:0.6534725427627563), translation: Vec3(x:0.16022956371307373, '
'y:0.5206693410873413, z:0.3277728259563446))',
'bevy_ui::focus::FocusPolicy': 'Block',
'bevy_ui::focus::Interaction': 'None',
'bevy_ui::focus::RelativeCursorPosition': '(normalized: "", normalized_visible_node_rect: (max: '
'Vec2(x:0.5714026093482971, y:0.42888906598091125), min: '
'Vec2(x:0.5780913233757019, y:0.20609822869300842)))',
'bevy_ui::measurement::ContentSize': '()',
'bevy_ui::ui_node::BackgroundColor': '(Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842))',
'bevy_ui::ui_node::BorderColor': '(Rgba(red:0.5714026093482971, green:0.42888906598091125, blue:0.5780913233757019, '
'alpha:0.20609822869300842))',
'bevy_ui::ui_node::CalculatedClip': '(clip: (max: Vec2(x:0.5714026093482971, y:0.42888906598091125), min: '
'Vec2(x:0.5780913233757019, y:0.20609822869300842)))',
'bevy_ui::ui_node::Node': '(calculated_size: Vec2(x:0.5714026093482971, y:0.42888906598091125), outline_offset: '
'0.5780913233757019, outline_width: 0.20609822869300842, stack_index: 62, unrounded_size: '
'Vec2(x:0.8235888481140137, y:0.6534725427627563))',
'bevy_ui::ui_node::Outline': '(color: Rgba(red:0.5714026093482971, green:0.42888906598091125, '
'blue:0.5780913233757019, alpha:0.20609822869300842), offset: VMax(0.4912964105606079), '
'width: Percent(0.6534725427627563))',
'bevy_ui::ui_node::Style': '(align_content: SpaceAround, align_items: Default, align_self: Baseline, aspect_ratio: '
'Some(0.5780913233757019), border: (bottom: Px(0.46258050203323364), left: '
'Vw(0.8235888481140137), right: VMin(0.8106188178062439), top: Auto), bottom: '
'Vh(0.49008557200431824), column_gap: Auto, direction: Inherit, display: None, flex_basis: '
'Percent(0.0445563830435276), flex_direction: Column, flex_grow: 0.6031906008720398, '
'flex_shrink: 0.38160598278045654, flex_wrap: Wrap, grid_auto_columns: "", grid_auto_flow: '
'RowDense, grid_auto_rows: "", grid_column: (end: "", span: "", start: ""), grid_row: '
'(end: "", span: "", start: ""), grid_template_columns: "", grid_template_rows: "", '
'height: Vw(0.17467059195041656), justify_content: FlexEnd, justify_items: Stretch, '
'justify_self: End, left: Px(0.45692843198776245), margin: (bottom: '
'VMax(0.9824132323265076), left: Vw(0.6133268475532532), right: Auto, top: '
'Vh(0.004055144265294075)), max_height: Px(0.1949533075094223), max_width: '
'Percent(0.5363451838493347), min_height: VMax(0.8981962203979492), min_width: '
'Percent(0.666689932346344), overflow: (x: Clip, y: Clip), padding: (bottom: '
'Vw(0.06499417871236801), left: Vh(0.32468828558921814), right: Vh(0.15641891956329346), '
'top: Px(0.9697836637496948)), position_type: Relative, right: Auto, row_gap: Auto, top: '
'Vw(0.3011642396450043), width: Vh(0.6578909158706665))',
'bevy_ui::ui_node::UiImage': '(flip_x: true, flip_y: false, texture: Weak(Uuid(uuid: '
'"73b3b118-7d01-4778-8bcc-4e79055f5d22")))',
'bevy_ui::ui_node::ZIndex': 'Local(54)',
'bevy_ui::widget::button::Button': '()',
'bevy_ui::widget::image::UiImageSize': '(size: Vec2(x:0.5714026093482971, y:0.42888906598091125))',
'bevy_ui::widget::label::Label': '()',
'bevy_ui::widget::text::TextFlags': '(needs_new_measure_func: true, needs_recompute: false)',
'bevy_window::window::PrimaryWindow': '()',
'bevy_window::window::Window': '(canvas: None, composite_alpha_mode: PostMultiplied, cursor: (grab_mode: Confined, '
'hit_test: true, icon: Default, visible: false), decorations: false, enabled_buttons: '
'(close: true, maximize: false, minimize: true), focused: false, ime_enabled: true, '
'ime_position: Vec2(x:0.8106188178062439, y:0.03440357372164726), internal: '
'(maximize_request: Some(false), minimize_request: None, physical_cursor_position: '
'None), mode: SizedFullscreen, name: None, position: Centered(Current), present_mode: '
'Immediate, prevent_default_event_handling: false, resizable: false, '
'resize_constraints: (max_height: 0.42126399278640747, max_width: 0.8268482089042664, '
'min_height: 0.2623211145401001, min_width: 0.17467059195041656), resolution: '
'(physical_height: 38, physical_width: 84, scale_factor: 0.36258742213249207, '
'scale_factor_override: Some(0.7678378224372864)), title: "hotmbsah", transparent: '
'false, visible: false, window_level: Normal, window_theme: "")'}

View File

@ -1,31 +0,0 @@
import bpy
import pytest
@pytest.fixture
def setup_data(request):
print("\nSetting up resources...")
schemaPath = "../../testing/bevy_example/assets/registry.json"
yield {"schema_path": schemaPath}
def finalizer():
print("\nPerforming teardown...")
registry = bpy.context.window_manager.components_registry
type_infos = registry.type_infos
object = bpy.context.object
remove_component_operator = bpy.ops.object.remove_bevy_component
for long_name in type_infos:
definition = type_infos[long_name]
component_name = definition["short_name"]
if component_name in object:
try:
remove_component_operator(component_name=component_name)
except Exception as error:
pass
request.addfinalizer(finalizer)
return None

View File

@ -1,257 +0,0 @@
import bpy
import pprint
from ..propGroups.conversions_to_prop_group import property_group_value_from_custom_property_value
from ..propGroups.conversions_from_prop_group import property_group_value_to_custom_property_value
from .component_values_shuffler import component_values_shuffler
from .expected_component_values import (expected_custom_property_values, expected_custom_property_values_randomized)
from ..components.metadata import get_bevy_component_value_by_long_name, get_bevy_components, upsert_bevy_component
from .setup_data import setup_data
def test_components_should_generate_correct_custom_properties(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
type_infos = registry.type_infos
object = bpy.context.object
add_component_operator = bpy.ops.object.add_bevy_component
errors = []
addable_components = []
added_components = []
custom_property_values = {}
for long_name in type_infos:
definition = type_infos[long_name]
long_name = definition["long_name"]
is_component = definition['isComponent'] if "isComponent" in definition else False
if not is_component:
continue
addable_components.append(long_name)
try:
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
added_components.append(long_name)
custom_property_values[long_name] = get_bevy_component_value_by_long_name(object, long_name)
assert get_bevy_component_value_by_long_name(object, long_name) == expected_custom_property_values[long_name]
except Exception as error:
errors.append(error)
pp = pprint.PrettyPrinter(depth=14, width=120)
print("CUSTOM PROPERTY VALUES")
pp.pprint(custom_property_values)
assert len(errors) == 0
assert len(added_components) == 173
def test_components_should_generate_correct_custom_properties_with_randomized_values(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
type_infos = registry.type_infos
object = bpy.context.object
add_component_operator = bpy.ops.object.add_bevy_component
errors = []
error_components = []
addable_components = []
added_components = []
custom_property_values = {}
for long_name in type_infos:
definition = type_infos[long_name]
long_name = definition["long_name"]
is_component = definition['isComponent'] if "isComponent" in definition else False
if not is_component:
continue
addable_components.append(long_name)
try:
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
component_values_shuffler(seed= 10, property_group=propertyGroup, definition=definition, registry=registry)
added_components.append(long_name)
custom_property_values[long_name] = get_bevy_component_value_by_long_name(object, long_name)
assert get_bevy_component_value_by_long_name(object, long_name) == expected_custom_property_values_randomized[long_name]
except Exception as error:
errors.append(error)
error_components.append(long_name)
pp = pprint.PrettyPrinter(depth=14, width=120)
print("CUSTOM PROPERTY VALUES")
pp.pprint(custom_property_values)
print("error_components", error_components)
assert len(errors) == 0
assert len(added_components) == 173
def test_components_should_generate_correct_propertyGroup_values_from_custom_properties(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
type_infos = registry.type_infos
object = bpy.context.object
add_component_operator = bpy.ops.object.add_bevy_component
errors = []
addable_components = []
added_components = []
failing_components = []
for long_name in type_infos:
definition = type_infos[long_name]
long_name = definition["long_name"]
is_component = definition['isComponent'] if "isComponent" in definition else False
if not is_component:
continue
addable_components.append(long_name)
try:
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
added_components.append(long_name)
# randomise values
component_values_shuffler(seed= 10, property_group=propertyGroup, definition=definition, registry=registry)
custom_property_value = get_bevy_component_value_by_long_name(object, long_name)
# first check if custom property value matches what we expect
assert custom_property_value == expected_custom_property_values_randomized[long_name]
# we update propgroup values from custom property values
property_group_value_from_custom_property_value(propertyGroup, definition, registry, custom_property_value, nesting = [])
# and then generate it back
custom_property_value_regen = property_group_value_to_custom_property_value(propertyGroup, definition, registry, None)
assert custom_property_value_regen == expected_custom_property_values_randomized[long_name]
# custom_property_values[long_name] = object[long_name]
#assert object[long_name] == expected_custom_property_values[long_name]
#print("CUSTOM PROPERTY ", object[long_name])
except Exception as error:
errors.append(error)
failing_components.append(long_name)
for index, error in enumerate(errors):
print("ERROR", error, failing_components[index])
assert len(errors) == 0
assert len(added_components) == 173
def test_remove_components(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
type_infos = registry.type_infos
add_component_operator = bpy.ops.object.add_bevy_component
errors = []
addable_components = []
added_components = []
for long_name in type_infos:
definition = type_infos[long_name]
long_name = definition["long_name"]
is_component = definition['isComponent'] if "isComponent" in definition else False
if not is_component:
continue
addable_components.append(long_name)
try:
add_component_operator(component_type=long_name)
object = bpy.context.object
# print("propertyGroup", propertyGroup, propertyGroup.field_names)
added_components.append(long_name)
except Exception as error:
errors.append(error)
assert len(errors) == 0
# now test component removal
errors.clear()
remove_component_operator = bpy.ops.object.remove_bevy_component
for long_name in added_components:
try:
remove_component_operator(component_name=long_name)
except Exception as error:
errors.append(error)
assert len(errors) == 0
def test_copy_paste_components(setup_data):
context = bpy.context
registry = context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
long_name = "bevy_example::test_components::BasicTest"
# SOURCE object setup
add_component_operator = bpy.ops.object.add_bevy_component
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
object = context.object
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
setattr(propertyGroup, propertyGroup.field_names[0], 25.0)
copy_component_operator = bpy.ops.object.copy_bevy_component
copy_component_operator(source_component_name=long_name, source_object_name=object.name)
# ---------------------------------------
# TARGET object
bpy.ops.mesh.primitive_cube_add()
new_cube = bpy.context.selected_objects[0]
# change name
new_cube.name = "TargetCube"
target_components_metadata = new_cube.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
# first check that there is no component currently
assert component_meta == None
paste_component_operator = bpy.ops.object.paste_bevy_component
paste_component_operator()
target_components_metadata = new_cube.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
# now after pasting to the new object, it should have component meta
assert component_meta != None
# and then check if the propertyGroup of the target object is correct
propertyGroup = getattr(component_meta, property_group_name, None)
assert propertyGroup.field_names == ['a', 'b', 'c']
a_fieldValue = getattr(propertyGroup, propertyGroup.field_names[0])
assert a_fieldValue == 25.0

View File

@ -1,51 +0,0 @@
from ..propGroups.conversions_to_prop_group import parse_struct_string, parse_tuplestruct_string
def test_parse_tuplestruct_string():
assert parse_tuplestruct_string("(A)", start_nesting=1) == ['A']
assert parse_tuplestruct_string("[(A)]", start_nesting=1) == ['(A)']
assert parse_tuplestruct_string("(a: 45, b: 65)", start_nesting=1) == ['a: 45', 'b: 65']
assert parse_tuplestruct_string("[(a: 45, b: 65)]", start_nesting=1) == ['(a: 45, b: 65)']
assert parse_tuplestruct_string("45, 65, 'bla'", start_nesting=0) == ['45', '65', "'bla'"]
assert parse_tuplestruct_string("[(A), (B)]", start_nesting=1) == ['(A)', '(B)']
assert parse_tuplestruct_string("([(-1.8, 2.9), (0.0, -62)])", start_nesting=1) == ['[(-1.8, 2.9), (0.0, -62)]']
assert parse_tuplestruct_string("([(-1.8, 2.9), (0.0, -62)])", start_nesting=2) == ['(-1.8, 2.9)', '(0.0, -62)']
assert parse_tuplestruct_string("([(-1.8, 2.9), (0.0, -62), (25)])", start_nesting=2) == ['(-1.8, 2.9)', '(0.0, -62)', '(25)']
assert parse_tuplestruct_string("(Vec3(x:-2.0, y:120.0, z:1.0))", start_nesting=2) == ['x:-2.0', 'y:120.0', 'z:1.0']
assert parse_tuplestruct_string("(9)", start_nesting=1) == ['9']
assert parse_tuplestruct_string('("toto")', start_nesting=1) == ['"toto"']
assert parse_tuplestruct_string("(Rgba(red:0.0, green:0.2, blue:0.9, alpha:1.0))", start_nesting=1) == ['Rgba(red:0.0, green:0.2, blue:0.9, alpha:1.0)']
assert parse_tuplestruct_string("(Rgba(red:0.0, green:0.2, blue:0.9, alpha:1.0))", start_nesting=2) == ['red:0.0', 'green:0.2', 'blue:0.9', 'alpha:1.0']
assert parse_tuplestruct_string("([(-1.2, 2.9), (0.0, -62)])", start_nesting=2) == ['(-1.2, 2.9)', '(0.0, -62)']
assert parse_tuplestruct_string("([Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0), Rgba(red:1.0, green:0.0, blue:0.5, alpha:1.0)])", start_nesting=2) == ['Rgba(red:1.0, green:1.0, blue:0.0, alpha:1.0)', 'Rgba(red:1.0, green:0.0, blue:0.5, alpha:1.0)']
assert parse_tuplestruct_string('(7.2, 2607, "sdf")', start_nesting=1) == ['7.2', '2607', '"sdf"']
assert parse_tuplestruct_string('[a, b]', start_nesting=1) == ['a', 'b']
assert parse_tuplestruct_string('[]', start_nesting=1) == []
def test_parse_struct_string():
assert parse_struct_string("a: 45, b:65") == {'a': '45', 'b':'65'}
assert parse_struct_string("x:-2.0, y:120.0, z:1.0") == {'x': '-2.0', 'y':'120.0', 'z':'1.0'}
assert parse_struct_string("enabled: true") == {'enabled': 'true'}
assert parse_struct_string("(enabled: true)", start_nesting=1) == {'enabled': 'true'}
assert parse_struct_string("(filters: (25), memberships: (5))", start_nesting=1) == {'filters': '(25)', 'memberships':'(5)'}
assert parse_struct_string("groups: 0", start_nesting=0) == {'groups': '0'}
assert parse_struct_string("(groups: 0)", start_nesting=1) == {'groups': '0'}
assert parse_struct_string("(composite_mode: EnergyConserving, high_pass_frequency: 4.0, intensity: 0.0, low_frequency_boost: -6.0, low_frequency_boost_curvature: 4.1, prefilter_settings: (threshold: -5.1, threshold_softness: 2.1))", start_nesting=1) == {'composite_mode': 'EnergyConserving', 'high_pass_frequency': '4.0', 'intensity': '0.0', 'low_frequency_boost': '-6.0', 'low_frequency_boost_curvature': '4.1', 'prefilter_settings': '(threshold: -5.1, threshold_softness: 2.1)'}
assert parse_struct_string("dimensions: UVec3(x:0.0, y:0.0, z:0.0), dynamic_resizing: true, z_config: (far_z_mode: MaxLightRange, first_slice_depth: 0.0)") == {'dimensions': 'UVec3(x:0.0, y:0.0, z:0.0)', 'dynamic_resizing': 'true', 'z_config': '(far_z_mode: MaxLightRange, first_slice_depth: 0.0)'}
assert parse_struct_string('(inverse_bindposes: Strong(""), joints: [4294967295, 4294967295, 4294967295])', start_nesting=1) == {'inverse_bindposes': 'Strong("")', 'joints': '[4294967295, 4294967295, 4294967295]'}

View File

@ -1,22 +0,0 @@
import bpy
from .setup_data import setup_data
def test_blend(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
long_name = "bevy_example::test_components::BasicTest"
add_component_operator = bpy.ops.object.add_bevy_component
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
object = bpy.context.object
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
assert propertyGroup.field_names == ['a', 'b', 'c']

View File

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

View File

@ -1,150 +0,0 @@
import bpy
from .component_values_shuffler import component_values_shuffler
from ..components.metadata import get_bevy_component_value_by_long_name, get_bevy_components, upsert_bevy_component
from .setup_data import setup_data
def test_shuffler(setup_data):
registry = bpy.context.window_manager.components_registry
registry.schemaPath = setup_data["schema_path"]
bpy.ops.object.reload_registry()
type_infos = registry.type_infos
object = bpy.context.object
add_component_operator = bpy.ops.object.add_bevy_component
long_name = "bevy_example::test_components::BasicTest"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 10, property_group=propertyGroup, definition=definition, registry=registry)
assert getattr(propertyGroup, 'a') == 0.5714026093482971
assert getattr(propertyGroup, 'b') == 54
assert getattr(propertyGroup, 'c') == "psagopiu"
# Testing a more complex component
long_name = "bevy_example::test_components::NestingTestLevel2"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
#print("propertyGroup", object[long_name])
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == '(basic: (a: 0.5219839215278625, b: 38, c: "ljfywwrv"), color: (Rgba(red:0.2782765030860901, green:0.9174930453300476, blue:0.24890311062335968, alpha:0.815186083316803)), colors_list: ([Rgba(red:0.2523837685585022, green:0.5016026496887207, blue:0.317435085773468, alpha:0.8463277816772461), Rgba(red:0.945193886756897, green:0.4015909433364868, blue:0.9984470009803772, alpha:0.06219279021024704)]), enable: true, enum_inner: Wood, nested: (vec: (Vec3(x:0.1509154736995697, y:0.7055686116218567, z:0.5588918924331665))), text: "vgkrdwuc", toggle: (false))'
# And another complex component
long_name = "bevy_example::test_components::EnumComplex"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == 'StructLike(a: 0.41416797041893005, b: 38, c: "ljfywwrv")'
# And another complex component
long_name = "bevy_animation::AnimationPlayer"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == '(animation: "", paused: true)'
# And another complex component
long_name = "bevy_example::test_components::VecOfColors"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == '([Rgba(red:0.8066907525062561, green:0.9604947566986084, blue:0.2896253764629364, alpha:0.766107439994812), Rgba(red:0.7042198777198792, green:0.6613830327987671, blue:0.11016204953193665, alpha:0.02693677879869938)])'
# And another complex component
long_name = "bevy_example::test_components::VecOfF32s"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == '([0.8066907525062561, 0.9604947566986084])'
# And another complex component
long_name = "bevy_render::mesh::mesh::skinning::SkinnedMesh"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == '(inverse_bindposes: Weak(Uuid(uuid: "73b3b118-7d01-4778-8bcc-4e79055f5d22")), joints: [0, 0])'
# And another complex component
long_name = "bevy_render::camera::camera::CameraRenderGraph"
add_component_operator(component_type=long_name)
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
propertyGroup = getattr(component_meta, property_group_name, None)
definition = type_infos[long_name]
component_values_shuffler(seed= 17, property_group=propertyGroup, definition=definition, registry=registry)
print("propertyGroup", get_bevy_component_value_by_long_name(object, long_name))
# cheating / making things easier for us for complex types: we use the custom property value
assert get_bevy_component_value_by_long_name(object, long_name) == 'None'

View File

@ -123,5 +123,8 @@ General issues:
- [ ] for scenes, scan for used materials of all non instance objects (TODO: what about overrides ?)
- [x] remove BlueprintsList & replace is with assets list
- [ ] update main docs
- [ ] replace all references to the old 2 add-ons with those to Blenvy
- [ ] rename repo to "Blenvy"
clear && pytest -svv --blender-template ../../testing/bevy_example/art/testing_library.blend --blender-executable /home/ckaos/tools/blender/blender-4.1.0-linux-x64/blender tests/test_bevy_integration_prepare.py && pytest -svv --blender-executable /home/ckaos/tools/blender/blender-4.1.0-linux-x64/blender tests/test_bevy_integration.py

View File

@ -1,277 +0,0 @@
# gltf_auto_export
This [Blender addon](./)
- automatically exports your level/world from Blender to gltf whenever you save your Blend file.
- in Blueprints mode (highly recommended !) :
- supports automatic exports of used collections as [Gltf blueprints](../../crates/bevy_gltf_blueprints/README.md)
- supports any number of main/level scenes
- Blender scenes where you define your levels, and all collection instances are replaced with "pointers" to other gltf files (all automatic)
- supports any number of library scenes
- Blender scenes where you define the assets that you use in your levels, in the form of collections
- automatic export of **changed** objects & collections only ! a sort of "incremental export", where only the changed collections (if in use)
get exported when you save your blend file
## Installation:
* grab the latest release zip file
![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 ```gltf_auto_export.zip``` is stored
![blender addon install](./docs/blender_addon_install2.png)
## Usage:
> ***IMPORTANT***
if you have used a version of this add-on prior to v0.9, there was an issue that kept generating orphan (junk) data on every save !
You can easilly clean up that data
- go to orphan data:
![purge orphan data](./docs/purge_orphan1_data1.png)
- click on purge
![purge orphan data](./docs/purge_orphan1_data2.png)
- validate
![purge orphan data](./docs/purge_orphan1_data3.png)
This issue has been resolved in v0.9.
### Basics
* before it can automatically save to gltf, you need to configure it
* go to file => export => gltf auto export
![blender addon use](./docs/blender_addon_use.png)
* set the autoexport parameters in the **auto export** panel:
![blender addon use3](./docs/blender_addon_use3.png)
- export folder: root folder to export models too
- export scene settings: exports "global"/scene settings like ambient color, bloom, ao, etc
This automatically generates additional components at the scene level
- pick your main (level) scenes and/or library scenes (see the chapter about [Blueprints](#blueprints) and [multiple Blend filles workflow](#multiple-blend-file-workflow) below)
- click in the scene picker & select your scene
![select scene](./docs/blender_addon_add_scene.png)
- click on the "+" icon
![select scene2](./docs/blender_addon_add_scene2.png)
- your scene is added to the list
![select scene3](./docs/blender_addon_add_scene3.png)
- export blueprints: check this if you want to automatically export blueprints (default: True)
- blueprints path: the path to export blueprints to , relative to the main **export folder** (default: library)
- collection instances: select which option you want to use to deal with collection instances (aka combine mode) (both inside blueprint collections & main collections)
* split (default, highly recomended) : the addon will 'split out' any nested collections/ blueprints & export them
* embed: choose this option if you want to keep everything inside a gltf file (less efficient, not recomended)
* embedExternal: this will embed ONLY collection instances whose collections have not been found inside the current blend file
These options can also be **overridden** on a per collection instance basis: (if you want to split out most collection instances, but keep a few specific ones embeded
inside your gltf file)
![combine override](./docs/combine_override.png)
- simply add a custom property called **_combine** to the collection instance, and set it to one of the options above
please read the dedicated [section](#collection-instances--nested-blueprints) below for more information
- Export dynamic and static objects seperatly : For MAIN scenes only (aka levels), toggle this to generate 2 files per level:
- one with all dynamic data: collection or instances marked as dynamic (aka saveable)
- one with all static data: anything else that is NOT marked as dynamic, the file name will have the suffix **_dynamic**
Ie if you add a "Dynamic" custom property/ component to either your collection instances or your blueprint, you get a clean seperation between
- your static level data (anything that will never change during the lifetime of your Bevy app)
- your dynamic objects (anything that will change during the lifetime of your Bevy app, that can be saved & reloaded in save files for example)
- export materials library: check this if you want to automatically export material libraries (default: False)
please read the dedicated [section](#materials) below for more information
> This only works together with blueprints !
- materials path: where to export materials to
* and your standard gltf export parameters in the **gltf** panel
![blender addon use2](./docs/blender_addon_use2.png)
* click on "apply settings"
* now next time you save your blend file you will get an automatically exported gltf file (or more than one, depending on your settings, see below)
### Blueprints
You can enable this option to automatically replace all the **collection instances** inside your main scene with blueprints
- whenever you change your main scene (or your library scene , if that option is enabled), all your collection instances
* will be replaced with empties (this will not be visible to you)
* those empties will have additional custom properties / components : ```BlueprintName``` & ```SpawnHere```
* your main scene/ level will be exported to a much more trimmed down gltf file (see next point)
* all the original collections (that you used to create the instances) will be exported as **seperate gltf files** into the "library" folder
- this means you will have
* one small main gltf file (your level/world)
* as many gltf files as you have used collections in the main scene , in the library path you specified :
for the included [basic](../../examples/bevy_gltf_blueprints/basic/) example's [assets](../../examples/bevy_gltf_blueprints/basic/assets/), it looks something like this:
![library](./docs/exported_library_files.png)
the .blend file that they are generated from can be found [here](../../examples/bevy_gltf_blueprints/basic/assets/advanced.blend)
- the above only applies to collections that have **instances** in your main scene!
if you want a specific collection in your library to always get exported regardless of its use, you need to add
a **COLLECTION** (boolean) custom property called ```AutoExport``` set to true
> not at the object level ! the collection level !
![force-export](./docs/force_export.jpg)
It will get automatically exported like any of the "in-use" collections.
- you can also get an overview of all the exported collections in the export menu
![exported collections](./docs/exported_collections.png)
- there are some workflow specificities for multi blend file [workflows](#multiple-blend-file-workflow)
#### Collection instances & Nested blueprints
To maximise reuse of meshes/components etc, you can also nest ***collections instances*** inside collections (as normally in Blender), but also export each nested Blueprint as a seperate blueprints.
> Don't forget to choose the relevant option in the exporter settings (aka **"split"**)
> This replaces the previous "export nested blueprints" checkbox/ option
![instance combine mode](./docs/blender_addon_use4.png)
- To make things clearer:
![nested-blueprints](./docs/nested_blueprints.png)
- **Player2** & **Enemy** both use the **Humanoid_cactus** nested collection/blueprint, so **Humanoid_cactus** gets exported as a Blueprint for re-use ...but
- **Humanoid_cactus** is also made up of a main mesh & two instances of **Hand** , so **Hand** gets exported as a Blueprint for re-use ...but
- **Hand** is also made up of a main mesh & three instances of **Finger**, so **Finger** gets exported as a Blueprint for re-use
- The exported models in this case end up being:
![nested_blueprints2](./docs/nested_blueprints2.png)
- Note how **Player2.glb** is tiny, because most of its data is actually sotred in **Humanoid_cactus.glb**
- **Enemy.glb** is slightly bigger because that blueprints contains additional meshes
- All the intermediary blueprints got exported automatically, and all instances have been replaced with "empties" (see explanation in the **Process section** ) to minimize file size
- Compare this to the output **WITHOUT** the nested export option:
![nested_blueprints3](./docs/nested_blueprints3.png)
- less blueprints as the sub collections that are not in use somewhere directly are not exported
- **Player2.glb** & **Enemy.glb** are significantly larger
TLDR: smaller, more reuseable blueprints which can share sub-parts with other entities !
### Materials
You can enable this option to automatically generate a **material library** file that combines all the materials in use in your blueprints.
![material_library](./docs/blender_addon_materials2.png)
Since each blueprint is normally a completely independant gltf file, without this option, if you have a material with a large texture for example,
**ALL** of your blueprints using that material will embed that large texture, leading to **significant bloat & memory use**.
- When this option is enabled, you get a single material library per Blender project, and a **MaterialInfo** component is inserted into each object using a material.
- The correct material will then be inserted on the Bevy side (that loads any number of material libraries that you need) into the correct mesh (see the configuration
options in **bevy_gltf_blueprints** for more information on that)
- Only one material per object is supported at this stage, ie the last material slot's material is the one that is going to be used
![material_library](./docs/blender_addon_materials.png)
TLDR: Use this option to make sure that each blueprint file does not contain a copy of the same materials
### Multiple blend file workflow
If you want to use multiple blend files, use Blender's asset library etc, we got you coverred too !
There are only a few things to keep in mind
#### Assets/library/blueprints files
- mark your library scenes as specified above, but **do NOT** specify a **main** scene
- mark any collection in your scenes as "assets" (more convenient) or add the "AutoExport" custom property to the collection
- choose "split" for the combine mode (as you want your gltf blueprints to be saved for external use)
- do your Blender things as normal
- anytime you save your file, it will automatically export any relevant collections/blueprints
- (optional) activate the **material library** option, so you only have one set of material per asset library (recomended)
#### Level/world files
- mark your main scenes as specified above, but **do NOT** specify a **library** scene
- configure your asset libraries as you would usually do , I recomend using the "link" mode so that any changes to asset files are reflected correctly
- drag & drop any assets from the blueprints library (as you would normally do in Blender as well)
- choose "split" for the combine mode (as you want your gltf blueprints to be external usually & use the gltf files generated from your assets library)
- do your Blender things as normal
- anytime you save your file, it will automatically export your level(s)
Take a look at the [relevant](../../examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/) example for more [details](../../examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/art/)
### Internal Process overview
This is the internal logic of the export process with blueprints (simplified)
![process](./docs/process.svg)
ie this is an example scene...
![](./docs/workflow_original.jpg)
and what actually gets exported for the main scene/world/level
![](./docs/workflow_empties.jpg)
all collections instances replaced with empties, and all those collections exported to gltf files as seen above
## Development
- since the code has now been split up into multiple modules, to make your life easier, I highly recomend (if you are using vscode like me) to use
[this](https://marketplace.visualstudio.com/items?itemName=JacquesLucke.blender-development) excellent extension , works easilly and fast , even for the latest
versions of Blender (v4.0 as of this writing)
- this [article](https://polynook.com/learn/set-up-blender-addon-development-environment-in-windows) might also help out
(easy enough to get it working on linux too)
## 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

@ -1,69 +0,0 @@
- investigate remove_blueprints_list_from_main_scene (could be a case of changes to bpy.data not being applied immediatly)
- investigate clearing of changed_objects_per_scene
- it seems bevy_components does not trigger updates
- undo redo is ignored: ie save, do something, undo it, you still get changes
- [ ] serialize scene
- [ ] for collection instances:
* [ ] blueprints export should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported
* [ ] level exports should do the same
- [ ] add tests for the above
- [ ] look into caching for serialize scene
- [ ] replace field name based logic with type base logic
- [ ] to make things easier overall we need a mapping of Blueprints/Collections to
- [x] their instances
- [x] their objects/sub collections instances etc
- [ ] a mapping of objects to the blueprints they belong to
- [ ] things to alter/remove using the new & improved Blueprints/collections scanning and mapping
- [x] get_sub_collections => remove , but rewrite how BlueprintsList are generated
- [x] get_used_collections => remove , but rewrite how BlueprintsList are generated
- [x] get_exportable_collections => remove , but replace with new function to get exportable blueprints
- [x] get_collections_per_scene
- [x] get_collections_in_library
- [ ] traverse_tree => keep, used
- [x] find_layer_collection_recursive => remove, unused
- [ ] recurLayerCollection => unclear, analyse
- [x] find_collection_ascendant_target_collection => remove, double check
- [x] set_active_collection => keep, used
- [x] get_source_scene => remove, unused
- [x] assets_list["BlueprintsList"]
BLUEPRINTS LIST {'Blueprint1': [], 'Blueprint6_animated': [], 'Blueprint4_nested': ['Blueprint3'], 'Blueprint3': [], 'Blueprint7_hierarchy': [], 'External_blueprint': [], 'External_blueprint2': ['External_blueprint3'], 'External_blueprint3': [], 'Blueprint8_animated_no_bones': []}
- [x] internal_collections => replace with "internal_collections" or "local_collections"
- [x] fix COMBINE MODE passed as int instead of enum value
=> comes from our custom logic for add_on prefs
- [ ] double check compares to "None" values
- [ ] add tests for relative/absolute paths
- [x] move all things that alter data "permanently" to pre-save
- [x] lighting/ scene components injection
- [x] blueprintNames ?
- [x] or more simple: just remove them after save as we do for others: lighting_components
- [ ] if we want the blueprintsList / future paths of blueprints to be present inside external assets, we are going to need to keep them around, ie: inject them in pre-save & not remove them
- [ ] update cleanup_materials
- [x] remove legacy mode
- [x] from auto_export
- [x] from rust code
- [x] from examples
- [x] added notes & workaround information in docs
- [ ] remove bulk of tracker related code
- [ ] clean up
- [x] split up change detection in settings to its own panel
Change storage of 'blueprint' assets : (from BlueprintsList)
- store at the SCENE level: a list/map of assets
- asset name + asset path
- the asset PATH is determined by the export output folder parameters
- make asset storage generic enough to allow adding additional asset types
- get inspired by bevy_asset_loader ?

View File

@ -1,202 +0,0 @@
bl_info = {
"name": "gltf_auto_export",
"author": "kaosigh",
"version": (0, 16, 0),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
"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": "Import-Export"
}
import os
import json
import bpy
from bpy.props import (StringProperty, BoolProperty, IntProperty, PointerProperty)
# from .extension import ExampleExtensionProperties, GLTF_PT_UserExtensionPanel, unregister_panel
from .auto_export.operators import AutoExportGLTF
from .auto_export.tracker import AutoExportTracker
from .auto_export.preferences import (AutoExportGltfAddonPreferences)
from .auto_export.internals import (SceneLink,
SceneLinks,
CollectionToExport,
BlueprintsToExport,
CUSTOM_PG_sceneName
)
from .ui.main import (GLTF_PT_auto_export_change_detection, GLTF_PT_auto_export_changes_list, GLTF_PT_auto_export_main,
GLTF_PT_auto_export_root,
GLTF_PT_auto_export_general,
GLTF_PT_auto_export_scenes,
GLTF_PT_auto_export_blueprints,
SCENE_UL_GLTF_auto_export,
GLTF_PT_auto_export_SidePanel
)
from .ui.operators import (OT_OpenFolderbrowser, SCENES_LIST_OT_actions)
from .helpers.generate_complete_preferences_dict import generate_complete_preferences_dict_gltf
######################################################
"""
# glTF extensions are named following a convention with known prefixes.
# See: https://github.com/KhronosGroup/glTF/tree/main/extensions#about-gltf-extensions
# also: https://github.com/KhronosGroup/glTF/blob/main/extensions/Prefixes.md
glTF_extension_name = "EXT_auto_export"
# Support for an extension is "required" if a typical glTF viewer cannot be expected
# to load a given model without understanding the contents of the extension.
# For example, a compression scheme or new image format (with no fallback included)
# would be "required", but physics metadata or app-specific settings could be optional.
extension_is_required = False
class AutoExportExtensionProperties(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name=bl_info["name"],
description='Include this extension in the exported glTF file.',
default=True
) # type: ignore
class glTF2ExportUserExtension:
def __init__(self):
print("init extension", self)
# We need to wait until we create the gltf2UserExtension to import the gltf2 modules
# Otherwise, it may fail because the gltf2 may not be loaded yet
from io_scene_gltf2.io.com.gltf2_io_extensions import Extension
self.Extension = Extension
self.properties = bpy.context.scene.AutoExportExtensionProperties
def gather_node_hook(self, gltf2_object, blender_object, export_settings):
print("fooo", self)
if self.properties.enabled:
if gltf2_object.extensions is None:
gltf2_object.extensions = {}
print("bla bla")
gltf2_object.extensions[glTF_extension_name] = self.Extension(
name=glTF_extension_name,
extension={"auto_export_blueprints": self.properties.auto_export_blueprints},
required=extension_is_required
)
def gather_animation_hook():
pass
def gather_gltf_hook(self, active_scene_idx, scenes, animations, export_settings):
if self.properties.enabled:
print("extension enabled")
#print("gather_gltf_hook", self, active_scene_idx, scenes, animations, export_settings)"""
#see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py
classes = [
SceneLink,
SceneLinks,
CUSTOM_PG_sceneName,
SCENE_UL_GLTF_auto_export,
SCENES_LIST_OT_actions,
OT_OpenFolderbrowser,
AutoExportGLTF,
CollectionToExport,
BlueprintsToExport,
GLTF_PT_auto_export_main,
GLTF_PT_auto_export_root,
GLTF_PT_auto_export_general,
GLTF_PT_auto_export_change_detection,
GLTF_PT_auto_export_scenes,
GLTF_PT_auto_export_blueprints,
GLTF_PT_auto_export_SidePanel,
AutoExportTracker,
]
def cleanup_file():
gltf_filepath = "/home/ckaos/projects/bevy/Blender_bevy_components_worklflow/testing/bevy_example/assets/____dummy____.glb"
if os.path.exists(gltf_filepath):
os.remove(gltf_filepath)
return None
else:
return 1
def glTF2_post_export_callback(data):
#print("post_export", data)
bpy.context.window_manager.auto_export_tracker.export_finished()
gltf_settings_backup = bpy.context.window_manager.gltf_settings_backup
gltf_filepath = data["gltf_filepath"]
gltf_export_id = data['gltf_export_id']
if gltf_export_id == "gltf_auto_export":
# some more absurdity: apparently the file is not QUITE done when the export callback is called, so we have to introduce this timer to remove the temporary file correctly
bpy.context.window_manager.auto_export_tracker.dummy_file_path = gltf_filepath
try:
bpy.app.timers.unregister(cleanup_file)
except:pass
bpy.app.timers.register(cleanup_file, first_interval=1)
# get the parameters
scene = bpy.context.scene
if "glTF2ExportSettings" in scene:
settings = scene["glTF2ExportSettings"]
export_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_gltf_settings")
# now write new settings
export_settings.clear()
current_gltf_settings = generate_complete_preferences_dict_gltf(dict(settings))
export_settings.write(json.dumps(current_gltf_settings))
# now reset the original gltf_settings
if gltf_settings_backup != "":
scene["glTF2ExportSettings"] = json.loads(gltf_settings_backup)
else:
if "glTF2ExportSettings" in scene:
del scene["glTF2ExportSettings"]
bpy.context.window_manager.gltf_settings_backup = ""
# the absurd length one has to go through to RESET THE OPERATOR because it has global state !!!!! AAAAAHHH
last_operator = bpy.context.window_manager.auto_export_tracker.last_operator
last_operator.filepath = ""
last_operator.gltf_export_id = ""
def menu_func_import(self, context):
self.layout.operator(AutoExportGLTF.bl_idname, text="glTF auto Export (.glb/gltf)")
from bpy.app.handlers import persistent
@persistent
def post_update(scene, depsgraph):
bpy.context.window_manager.auto_export_tracker.deps_post_update_handler( scene, depsgraph)
@persistent
def post_save(scene, depsgraph):
bpy.context.window_manager.auto_export_tracker.save_handler( scene, depsgraph)
def register():
for cls in classes:
bpy.utils.register_class(cls)
# for some reason, adding these directly to the tracker class in register() do not work reliably
bpy.app.handlers.depsgraph_update_post.append(post_update)
bpy.app.handlers.save_post.append(post_save)
# add our addon to the toolbar
bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.types.WindowManager.gltf_settings_backup = StringProperty(default="")
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
bpy.app.handlers.depsgraph_update_post.remove(post_update)
bpy.app.handlers.save_post.remove(post_save)
if "gltf_auto_export" == "__main__":
register()

View File

@ -1,183 +0,0 @@
import copy
import json
import os
from types import SimpleNamespace
import bpy
import traceback
from .preferences import AutoExportGltfAddonPreferences
from .get_blueprints_to_export import get_blueprints_to_export
from .get_levels_to_export import get_levels_to_export
from .get_standard_exporter_settings import get_standard_exporter_settings
from .export_main_scenes import export_main_scene
from .export_blueprints import export_blueprints
from ..helpers.helpers_scenes import (get_scenes, )
from ..helpers.helpers_blueprints import blueprints_scan
from ..modules.export_materials import cleanup_materials, export_materials
from ..modules.bevy_scene_components import remove_scene_components, upsert_scene_components
"""this is the main 'central' function for all auto export """
def auto_export(changes_per_scene, changed_export_parameters, addon_prefs):
# have the export parameters (not auto export, just gltf export) have changed: if yes (for example switch from glb to gltf, compression or not, animations or not etc), we need to re-export everything
print ("changed_export_parameters", changed_export_parameters)
try:
# path to the current blend file
file_path = bpy.data.filepath
# Get the folder
blend_file_path = os.path.dirname(file_path)
# get the preferences for our addon
export_root_folder = getattr(addon_prefs, "export_root_folder")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_models_path = os.path.join(blend_file_path, export_output_folder)
#should we use change detection or not
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_scene_settings = getattr(addon_prefs,"export_scene_settings")
do_export_blueprints = getattr(addon_prefs,"export_blueprints")
export_materials_library = getattr(addon_prefs,"export_materials_library")
print("export_materials_library", export_materials_library)
# standard gltf export settings are stored differently
standard_gltf_exporter_settings = get_standard_exporter_settings()
gltf_extension = standard_gltf_exporter_settings.get("export_format", 'GLB')
gltf_extension = '.glb' if gltf_extension == 'GLB' else '.gltf'
# generate the actual complete output path
export_blueprints_path = os.path.join(blend_file_path, export_root_folder, getattr(addon_prefs,"export_blueprints_path"))
export_levels_path = os.path.join(blend_file_path, export_root_folder, getattr(addon_prefs, "export_levels_path"))
print("export_blueprints_path", export_blueprints_path)
# here we do a bit of workaround by creating an override # TODO: do this at the "UI" level
print("collection_instances_combine_mode", addon_prefs.collection_instances_combine_mode)
"""if hasattr(addon_prefs, "__annotations__") :
tmp = {}
for k in AutoExportGltfAddonPreferences.__annotations__:
item = AutoExportGltfAddonPreferences.__annotations__[k]
#print("tutu",k, item.keywords.get('default', None) )
default = item.keywords.get('default', None)
tmp[k] = default
for (k, v) in addon_prefs.properties.items():
tmp[k] = v
addon_prefs = SimpleNamespace(**tmp) #copy.deepcopy(addon_prefs)
addon_prefs.__annotations__ = tmp"""
addon_prefs.export_blueprints_path = export_blueprints_path
addon_prefs.export_levels_path = export_levels_path
addon_prefs.export_gltf_extension = gltf_extension
addon_prefs.export_models_path = export_models_path
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
print("main scenes", main_scene_names, "library_scenes", library_scene_names)
print("export_output_folder", export_output_folder)
blueprints_data = blueprints_scan(level_scenes, library_scenes, addon_prefs)
blueprints_per_scene = blueprints_data.blueprints_per_scenes
internal_blueprints = [blueprint.name for blueprint in blueprints_data.internal_blueprints]
external_blueprints = [blueprint.name for blueprint in blueprints_data.external_blueprints]
if export_scene_settings:
# inject/ update scene components
upsert_scene_components(level_scenes)
#inject/ update light shadow information
for light in bpy.data.lights:
enabled = 'true' if light.use_shadow else 'false'
light['BlenderLightShadows'] = f"(enabled: {enabled}, buffer_bias: {light.shadow_buffer_bias})"
# export
if do_export_blueprints:
print("EXPORTING")
# get blueprints/collections infos
(blueprints_to_export) = get_blueprints_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs)
# get level/main scenes infos
(main_scenes_to_export) = get_levels_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs)
# since materials export adds components we need to call this before blueprints are exported
# export materials & inject materials components into relevant objects
if export_materials_library:
export_materials(blueprints_data.blueprint_names, library_scenes, blend_file_path, addon_prefs)
# update the list of tracked exports
exports_total = len(blueprints_to_export) + len(main_scenes_to_export) + (1 if export_materials_library else 0)
bpy.context.window_manager.auto_export_tracker.exports_total = exports_total
bpy.context.window_manager.auto_export_tracker.exports_count = exports_total
bpy.context.window_manager.exportedCollections.clear()
for blueprint in blueprints_to_export:
bla = bpy.context.window_manager.exportedCollections.add()
bla.name = blueprint.name
print("-------------------------------")
#print("collections: all:", collections)
#print("collections: not found on disk:", collections_not_on_disk)
print("BLUEPRINTS: local/internal:", internal_blueprints)
print("BLUEPRINTS: external:", external_blueprints)
print("BLUEPRINTS: per_scene:", blueprints_per_scene)
print("-------------------------------")
print("BLUEPRINTS: to export:", [blueprint.name for blueprint in blueprints_to_export])
print("-------------------------------")
print("MAIN SCENES: to export:", main_scenes_to_export)
print("-------------------------------")
# backup current active scene
old_current_scene = bpy.context.scene
# backup current selections
old_selections = bpy.context.selected_objects
# first export any main/level/world scenes
if len(main_scenes_to_export) > 0:
print("export MAIN scenes")
for scene_name in main_scenes_to_export:
print(" exporting scene:", scene_name)
export_main_scene(bpy.data.scenes[scene_name], blend_file_path, addon_prefs, blueprints_data)
# now deal with blueprints/collections
do_export_library_scene = not export_change_detection or changed_export_parameters or len(blueprints_to_export) > 0
if do_export_library_scene:
print("export LIBRARY")
# we only want to go through the library scenes where our blueprints to export are present
"""for (scene_name, blueprints_to_export) in blueprints_per_scene.items():
print(" exporting blueprints from scene:", scene_name)
print(" blueprints to export", blueprints_to_export)"""
export_blueprints(blueprints_to_export, blend_file_path, addon_prefs, blueprints_data)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
# reset selections
for obj in old_selections:
obj.select_set(True)
if export_materials_library:
cleanup_materials(blueprints_data.blueprint_names, library_scenes)
else:
for scene_name in main_scene_names:
export_main_scene(bpy.data.scenes[scene_name], blend_file_path, addon_prefs, [])
except Exception as error:
print(traceback.format_exc())
def error_message(self, context):
self.layout.label(text="Failure during auto_export: Error: "+ str(error))
bpy.context.window_manager.popup_menu(error_message, title="Error", icon='ERROR')
finally:
# FIXME: error handling ? also redundant
[main_scene_names, main_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
if export_scene_settings:
# inject/ update scene components
remove_scene_components(main_scenes)

View File

@ -1,39 +0,0 @@
import json
import bpy
"""
This should ONLY be run when actually doing exports/aka calling auto_export function, because we only care about the difference in settings between EXPORTS
"""
def did_export_settings_change():
# compare both the auto export settings & the gltf settings
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else None
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else None
current_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None
current_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
#check if params have changed
# if there were no setting before, it is new, we need export
changed = False
if previous_auto_settings == None:
print("previous settings missing, exporting")
changed = True
elif previous_gltf_settings == None:
print("previous gltf settings missing, exporting")
changed = True
else:
auto_settings_changed = sorted(json.loads(previous_auto_settings.as_string()).items()) != sorted(json.loads(current_auto_settings.as_string()).items()) if current_auto_settings != None else False
gltf_settings_changed = sorted(json.loads(previous_gltf_settings.as_string()).items()) != sorted(json.loads(current_gltf_settings.as_string()).items()) if current_gltf_settings != None else False
"""print("auto settings previous", sorted(json.loads(previous_auto_settings.as_string()).items()))
print("auto settings current", sorted(json.loads(current_auto_settings.as_string()).items()))
print("auto_settings_changed", auto_settings_changed)"""
"""print("gltf settings previous", sorted(json.loads(previous_gltf_settings.as_string()).items()))
print("gltf settings current", sorted(json.loads(current_gltf_settings.as_string()).items()))
print("gltf_settings_changed", gltf_settings_changed)"""
changed = auto_settings_changed or gltf_settings_changed
return changed

View File

@ -1,44 +0,0 @@
import os
import bpy
from ..constants import TEMPSCENE_PREFIX
from ..helpers.generate_and_export import generate_and_export
from .export_gltf import (generate_gltf_export_preferences)
from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into
def export_blueprints(blueprints, blend_file_path, addon_prefs, blueprints_data):
export_blueprints_path = getattr(addon_prefs,"export_blueprints_path")
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
try:
# save current active collection
active_collection = bpy.context.view_layer.active_layer_collection
export_materials_library = getattr(addon_prefs,"export_materials_library")
for blueprint in blueprints:
print("exporting collection", blueprint.name)
gltf_output_path = os.path.join(export_blueprints_path, blueprint.name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True}
# if we are using the material library option, do not export materials, use placeholder instead
if export_materials_library:
export_settings['export_materials'] = 'PLACEHOLDER'
collection = bpy.data.collections[blueprint.name]
generate_and_export(
addon_prefs,
temp_scene_name=TEMPSCENE_PREFIX+collection.name,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(collection, temp_collection, blueprints_data=blueprints_data, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=collection, temp_scene=temp_scene, **params)
)
# reset active collection to the one we save before
bpy.context.view_layer.active_layer_collection = active_collection
except Exception as error:
print("failed to export collections to gltf: ", error)
raise error

View File

@ -1,77 +0,0 @@
import json
import os
import bpy
from .get_standard_exporter_settings import get_standard_exporter_settings
from .preferences import (AutoExportGltfPreferenceNames)
def generate_gltf_export_preferences(addon_prefs):
# default values
gltf_export_preferences = dict(
# export_format= 'GLB', #'GLB', 'GLTF_SEPARATE', 'GLTF_EMBEDDED'
check_existing=False,
use_selection=False,
use_visible=True, # Export visible and hidden objects. See Object/Batch Export to skip.
use_renderable=False,
use_active_collection= False,
use_active_collection_with_nested=False,
use_active_scene = False,
export_cameras=True,
export_extras=True, # For custom exported properties.
export_lights=True,
#export_texcoords=True,
#export_normals=True,
# here add draco settings
#export_draco_mesh_compression_enable = False,
#export_tangents=False,
#export_materials
#export_colors=True,
#export_attributes=True,
#use_mesh_edges
#use_mesh_vertices
#export_yup=True,
#export_skins=True,
#export_morph=False,
#export_apply=False,
#export_animations=False,
#export_optimize_animation_size=False
)
for key in addon_prefs.__annotations__.keys():
if str(key) not in AutoExportGltfPreferenceNames:
#print("overriding setting", key, "value", getattr(addon_prefs,key))
gltf_export_preferences[key] = getattr(addon_prefs, key)
standard_gltf_exporter_settings = get_standard_exporter_settings()
constant_keys = [
'use_selection',
'use_visible',
'use_active_collection',
'use_active_collection_with_nested',
'use_active_scene',
'export_cameras',
'export_extras', # For custom exported properties.
'export_lights',
]
# a certain number of essential params should NEVER be overwritten , no matter the settings of the standard exporter
for key in standard_gltf_exporter_settings.keys():
if str(key) not in constant_keys:
gltf_export_preferences[key] = standard_gltf_exporter_settings.get(key)
return gltf_export_preferences
#https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf
def export_gltf (path, export_settings):
settings = {**export_settings, "filepath": path}
# print("export settings",settings)
os.makedirs(os.path.dirname(path), exist_ok=True)
#bpy.ops.export_scene.gltf(**settings)

View File

@ -1,77 +0,0 @@
import os
import bpy
from pathlib import Path
from ..constants import TEMPSCENE_PREFIX
from ..helpers.generate_and_export import generate_and_export
from .export_gltf import (generate_gltf_export_preferences, export_gltf)
from ..modules.bevy_dynamic import is_object_dynamic, is_object_static
from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into
from ..helpers.helpers_blueprints import inject_blueprints_list_into_main_scene, remove_blueprints_list_from_main_scene
def export_main_scene(scene, blend_file_path, addon_prefs, blueprints_data):
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_root_folder = getattr(addon_prefs, "export_root_folder")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_levels_path = getattr(addon_prefs,"export_levels_path")
export_blueprints = getattr(addon_prefs,"export_blueprints")
export_separate_dynamic_and_static_objects = getattr(addon_prefs, "export_separate_dynamic_and_static_objects")
export_settings = { **gltf_export_preferences,
'use_active_scene': True,
'use_active_collection':True,
'use_active_collection_with_nested':True,
'use_visible': False,
'use_renderable': False,
'export_apply':True
}
if export_blueprints :
gltf_output_path = os.path.join(export_levels_path, scene.name)
inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs)
return
if export_separate_dynamic_and_static_objects:
#print("SPLIT STATIC AND DYNAMIC")
# first export static objects
generate_and_export(
addon_prefs,
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, blueprints_data=blueprints_data, filter=is_object_static, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
# then export all dynamic objects
gltf_output_path = os.path.join(export_levels_path, scene.name+ "_dynamic")
generate_and_export(
addon_prefs,
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, blueprints_data=blueprints_data, filter=is_object_dynamic, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
else:
#print("NO SPLIT")
generate_and_export(
addon_prefs,
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, blueprints_data=blueprints_data, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
else:
gltf_output_path = os.path.join(export_root_folder, export_output_folder, scene.name)
print(" exporting gltf to", gltf_output_path, ".gltf/glb")
export_gltf(gltf_output_path, export_settings)
remove_blueprints_list_from_main_scene(scene)

View File

@ -1,60 +0,0 @@
import bpy
import os
from ..helpers.helpers_scenes import (get_scenes, )
from ..helpers.helpers_blueprints import find_blueprints_not_on_disk
# TODO: this should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported
def get_blueprints_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension", ".glb")
export_blueprints_path = getattr(addon_prefs,"export_blueprints_path", "")
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
internal_blueprints = blueprints_data.internal_blueprints
blueprints_to_export = internal_blueprints # just for clarity
# print("export_change_detection", export_change_detection, "changed_export_parameters", changed_export_parameters, "changes_per_scene", changes_per_scene)
# if the export parameters have changed, bail out early
# we need to re_export everything if the export parameters have been changed
if export_change_detection and not changed_export_parameters:
changed_blueprints = []
# first check if all collections have already been exported before (if this is the first time the exporter is run
# in your current Blender session for example)
blueprints_not_on_disk = find_blueprints_not_on_disk(internal_blueprints, export_blueprints_path, export_gltf_extension)
for scene in library_scenes:
if scene.name in changes_per_scene:
changed_objects = list(changes_per_scene[scene.name].keys())
changed_blueprints = [blueprints_data.blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_data.blueprints_from_objects]
# we only care about local blueprints/collections
changed_local_blueprints = [blueprint for blueprint in changed_blueprints if blueprint.name in blueprints_data.blueprints_per_name.keys() and blueprint.local]
# FIXME: double check this: why are we combining these two ?
changed_blueprints += changed_local_blueprints
blueprints_to_export = list(set(changed_blueprints + blueprints_not_on_disk))
# filter out blueprints that are not marked & deal with the different combine modes
# we check for blueprint & object specific overrides ...
filtered_blueprints = []
for blueprint in blueprints_to_export:
if blueprint.marked:
filtered_blueprints.append(blueprint)
else:
blueprint_instances = blueprints_data.internal_collection_instances.get(blueprint.name, [])
# print("INSTANCES", blueprint_instances, blueprints_data.internal_collection_instances)
# marked blueprints that have changed are always exported, regardless of whether they are in use (have instances) or not
for blueprint_instance in blueprint_instances:
combine_mode = blueprint_instance['_combine'] if '_combine' in blueprint_instance else collection_instances_combine_mode
if combine_mode == "Split": # we only keep changed blueprints if mode is set to split for at least one instance (aka if ALL instances of a blueprint are merged, do not export ? )
filtered_blueprints.append(blueprint)
blueprints_to_export = list(set(filtered_blueprints))
# changed/all blueprints to export
return (blueprints_to_export)

View File

@ -1,51 +0,0 @@
import bpy
from ..helpers.helpers_blueprints import check_if_blueprint_on_disk
from ..helpers.helpers_scenes import (get_scenes, )
# IF collection_instances_combine_mode is not 'split' check for each scene if any object in changes_per_scene has an instance in the scene
def changed_object_in_scene(scene_name, changes_per_scene, blueprints_data, collection_instances_combine_mode):
# Embed / EmbedExternal
blueprints_from_objects = blueprints_data.blueprints_from_objects
blueprint_instances_in_scene = blueprints_data.blueprint_instances_per_main_scene.get(scene_name, None)
if blueprint_instances_in_scene is not None:
changed_objects = [object_name for change in changes_per_scene.values() for object_name in change.keys()]
changed_blueprints = [blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_from_objects]
changed_blueprints_with_instances_in_scene = [blueprint for blueprint in changed_blueprints if blueprint.name in blueprint_instances_in_scene.keys()]
changed_blueprint_instances= [object for blueprint in changed_blueprints_with_instances_in_scene for object in blueprint_instances_in_scene[blueprint.name]]
# print("changed_blueprint_instances", changed_blueprint_instances,)
level_needs_export = False
for blueprint_instance in changed_blueprint_instances:
blueprint = blueprints_data.blueprint_name_from_instances[blueprint_instance]
combine_mode = blueprint_instance['_combine'] if '_combine' in blueprint_instance else collection_instances_combine_mode
#print("COMBINE MODE FOR OBJECT", combine_mode)
if combine_mode == 'Embed':
level_needs_export = True
break
elif combine_mode == 'EmbedExternal' and not blueprint.local:
level_needs_export = True
break
# changes => list of changed objects (regardless of wether they have been changed in main scene or in lib scene)
# wich of those objects are blueprint instances
# we need a list of changed objects that are blueprint instances
return level_needs_export
return False
# this also takes the split/embed mode into account: if a collection instance changes AND embed is active, its container level/world should also be exported
def get_levels_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension")
export_levels_path = getattr(addon_prefs, "export_levels_path")
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
# determine list of main scenes to export
# we have more relaxed rules to determine if the main scenes have changed : any change is ok, (allows easier handling of changes, render settings etc)
main_scenes_to_export = [scene_name for scene_name in main_scene_names if not export_change_detection or changed_export_parameters or scene_name in changes_per_scene.keys() or changed_object_in_scene(scene_name, changes_per_scene, blueprints_data, collection_instances_combine_mode) or not check_if_blueprint_on_disk(scene_name, export_levels_path, export_gltf_extension) ]
return (main_scenes_to_export)

View File

@ -1,14 +0,0 @@
import bpy
import json
def get_standard_exporter_settings():
standard_gltf_exporter_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
if standard_gltf_exporter_settings != None:
try:
standard_gltf_exporter_settings = json.loads(standard_gltf_exporter_settings.as_string())
except:
standard_gltf_exporter_settings = {}
else:
standard_gltf_exporter_settings = {}
return standard_gltf_exporter_settings

View File

@ -1,22 +0,0 @@
import bpy
class SceneLink(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="") # type: ignore
scene: bpy.props.PointerProperty(type=bpy.types.Scene) # type: ignore
class SceneLinks(bpy.types.PropertyGroup):
name = bpy.props.StringProperty(name="List of scenes to export", default="Unknown")
items: bpy.props.CollectionProperty(type = SceneLink) # type: ignore
class CUSTOM_PG_sceneName(bpy.types.PropertyGroup):
name: bpy.props.StringProperty() # type: ignore
display: bpy.props.BoolProperty() # type: ignore
class CollectionToExport(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="") # type: ignore
class BlueprintsToExport(bpy.types.PropertyGroup):
name = bpy.props.StringProperty(name="List of collections to export", default="Unknown")
items: bpy.props.CollectionProperty(type = CollectionToExport) # type: ignore

View File

@ -1,468 +0,0 @@
import json
import bpy
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
from bpy.props import (IntProperty, StringProperty, BoolProperty)
from ..ui.operators import OT_OpenFolderbrowser, draw_folder_browser
#from ..ui.main import GLTF_PT_auto_export_general, GLTF_PT_auto_export_main, GLTF_PT_auto_export_root
from .preferences import (AutoExportGltfAddonPreferences, AutoExportGltfPreferenceNames)
from .auto_export import auto_export
from ..helpers.generate_complete_preferences_dict import generate_complete_preferences_dict_auto
from ..helpers.serialize_scene import serialize_scene
def bubble_up_changes(object, changes_per_scene):
if object.parent:
changes_per_scene[object.parent.name] = bpy.data.objects[object.parent.name]
bubble_up_changes(object.parent, changes_per_scene)
class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences):#, ExportHelper):
"""auto export gltf"""
#bl_idname = "object.xxx"
bl_idname = "export_scenes.auto_gltf"
bl_label = "Apply settings"
bl_options = {'PRESET'} # we do not add UNDO otherwise it leads to an invisible operation that resets the state of the saved serialized scene, breaking compares for normal undo/redo operations
# ExportHelper mixin class uses this
#filename_ext = ''
#filepath: bpy.props.StringProperty(subtype="FILE_PATH", default="") # type: ignore
#list of settings (other than purely gltf settings) whose change should trigger a re-generation of gltf files
white_list = [
'auto_export',
'export_root_folder',
'export_output_folder',
'export_change_detection',
'export_scene_settings',
'main_scene_names',
'library_scene_names',
'export_blueprints',
'export_blueprints_path',
'export_marked_assets',
'collection_instances_combine_mode',
'export_levels_path',
'export_separate_dynamic_and_static_objects',
'export_materials_library',
'export_materials_path',
]
@classmethod
def register(cls):
bpy.types.WindowManager.main_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="main scene", description="main_scene_picker", poll=cls.is_scene_ok)
bpy.types.WindowManager.library_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="library scene", description="library_scene_picker", poll=cls.is_scene_ok)
bpy.types.WindowManager.main_scenes_list_index = IntProperty(name = "Index for main scenes list", default = 0)
bpy.types.WindowManager.library_scenes_list_index = IntProperty(name = "Index for library scenes list", default = 0)
cls.main_scenes_index = 0
cls.library_scenes_index = 0
@classmethod
def unregister(cls):
del bpy.types.WindowManager.main_scene
del bpy.types.WindowManager.library_scene
del bpy.types.WindowManager.main_scenes_list_index
del bpy.types.WindowManager.library_scenes_list_index
def is_scene_ok(self, scene):
try:
operator = bpy.context.space_data.active_operator
return scene.name not in operator.main_scenes and scene.name not in operator.library_scenes
except:
return True
def format_settings(self):
# find all props to save
exceptional = [
# options that don't start with 'export_'
'collection_instances_combine_mode',
]
all_props = self.properties
export_props = {
x: getattr(self, x) for x in dir(all_props)
if (x.startswith("export_") or x in exceptional) and all_props.get(x) is not None
}
# we inject all that we need, the above is not sufficient
for (k, v) in self.properties.items():
if k in self.white_list or k not in AutoExportGltfPreferenceNames:
value = v
# FIXME: really weird having to do this
if k == "collection_instances_combine_mode":
value = self.collection_instances_combine_mode
if k == "export_materials":
value = self.export_materials
export_props[k] = value
# we add main & library scene names to our preferences
export_props['main_scene_names'] = list(map(lambda scene_data: scene_data.name, getattr(self,"main_scenes")))
export_props['library_scene_names'] = list(map(lambda scene_data: scene_data.name, getattr(self,"library_scenes")))
return export_props
def save_settings(self, context):
print("save settings")
auto_export_settings = self.format_settings()
self.properties['main_scene_names'] = auto_export_settings['main_scene_names']
self.properties['library_scene_names'] = auto_export_settings['library_scene_names']
stored_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings")
stored_settings.clear()
auto_export_settings = generate_complete_preferences_dict_auto(auto_export_settings)
stored_settings.write(json.dumps(auto_export_settings))
print("saved settings", auto_export_settings)
#print("saving settings", bpy.data.texts[".gltf_auto_export_settings"].as_string(), "raw", json.dumps(export_props))
def load_settings(self, context):
print("loading settings")
settings = None
try:
settings = bpy.data.texts[".gltf_auto_export_settings"].as_string()
settings = json.loads(settings)
except: pass
self.will_save_settings = False
if settings:
#print("loading settings in invoke AutoExportGLTF", settings)
try:
for (k, v) in settings.items():
#print("loading setting", k, v)
setattr(self, k, v)
self.will_save_settings = True
# Update filter if user saved settings
if hasattr(self, 'export_format'):
self.filter_glob = '*.glb' if self.export_format == 'GLB' else '*.gltf'
# inject scenes data
if hasattr(self, 'main_scene_names'):
main_scenes = self.main_scenes
main_scenes.clear()
for item_name in self.main_scene_names:
item = main_scenes.add()
item.name = item_name
if hasattr(self, 'library_scene_names'):
library_scenes = self.library_scenes
library_scenes.clear()
for item_name in self.library_scene_names:
item = library_scenes.add()
item.name = item_name
except Exception as error:
print("error", error)
self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings")
bpy.data.texts.remove(bpy.data.texts[".gltf_auto_export_settings"])
else:
self.will_save_settings = True
"""
This should ONLY be run when actually doing exports/aka calling auto_export function, because we only care about the difference in settings between EXPORTS
"""
def did_export_settings_change(self):
# compare both the auto export settings & the gltf settings
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else None
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else None
current_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None
current_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
#check if params have changed
# if there were no setting before, it is new, we need export
changed = False
if previous_auto_settings == None:
#print("previous settings missing, exporting")
changed = True
elif previous_gltf_settings == None:
#print("previous gltf settings missing, exporting")
previous_gltf_settings = bpy.data.texts.new(".gltf_auto_export_gltf_settings_previous")
previous_gltf_settings.write(json.dumps({}))
if current_gltf_settings == None:
current_gltf_settings = bpy.data.texts.new(".gltf_auto_export_gltf_settings")
current_gltf_settings.write(json.dumps({}))
changed = True
else:
auto_settings_changed = sorted(json.loads(previous_auto_settings.as_string()).items()) != sorted(json.loads(current_auto_settings.as_string()).items()) if current_auto_settings != None else False
gltf_settings_changed = sorted(json.loads(previous_gltf_settings.as_string()).items()) != sorted(json.loads(current_gltf_settings.as_string()).items()) if current_gltf_settings != None else False
"""print("auto settings previous", sorted(json.loads(previous_auto_settings.as_string()).items()))
print("auto settings current", sorted(json.loads(current_auto_settings.as_string()).items()))
print("auto_settings_changed", auto_settings_changed)
print("gltf settings previous", sorted(json.loads(previous_gltf_settings.as_string()).items()))
print("gltf settings current", sorted(json.loads(current_gltf_settings.as_string()).items()))
print("gltf_settings_changed", gltf_settings_changed)"""
changed = auto_settings_changed or gltf_settings_changed
# now write the current settings to the "previous settings"
if current_auto_settings != None:
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings_previous")
previous_auto_settings.clear()
previous_auto_settings.write(current_auto_settings.as_string()) # TODO : check if this is always valid
if current_gltf_settings != None:
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_gltf_settings_previous")
previous_gltf_settings.clear()
previous_gltf_settings.write(current_gltf_settings.as_string())
return changed
def did_objects_change(self):
# sigh... you need to save & reset the frame otherwise it saves the values AT THE CURRENT FRAME WHICH CAN DIFFER ACROSS SCENES
current_frames = [scene.frame_current for scene in bpy.data.scenes]
for scene in bpy.data.scenes:
scene.frame_set(0)
current_scene = bpy.context.window.scene
bpy.context.window.scene = bpy.data.scenes[0]
#serialize scene at frame 0
"""with bpy.context.temp_override(scene=bpy.data.scenes[1]):
bpy.context.scene.frame_set(0)"""
current = serialize_scene()
bpy.context.window.scene = current_scene
# reset previous frames
for (index, scene) in enumerate(bpy.data.scenes):
scene.frame_set(int(current_frames[index]))
previous_stored = bpy.data.texts[".TESTING"] if ".TESTING" in bpy.data.texts else None # bpy.data.texts.new(".TESTING")
if previous_stored == None:
previous_stored = bpy.data.texts.new(".TESTING")
previous_stored.write(current)
return {}
previous = json.loads(previous_stored.as_string())
current = json.loads(current)
changes_per_scene = {}
# TODO : how do we deal with changed scene names ???
for scene in current:
# print('scene', scene)
previous_object_names = list(previous[scene].keys())
current_object_names =list(current[scene].keys())
#print("previous_object_names", len(previous_object_names), previous_object_names)
#print("current_object_names", len(current_object_names), current_object_names)
"""if len(previous_object_names) > len(current_object_names):
print("removed")
if len(current_object_names) > len(previous_object_names):
print("added")"""
added = list(set(current_object_names) - set(previous_object_names))
removed = list(set(previous_object_names) - set(current_object_names))
"""print("removed", removed)
print("added",added)"""
for obj in added:
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][obj] = bpy.data.objects[obj]
# TODO: how do we deal with this, as we obviously do not have data for removed objects ?
for obj in removed:
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][obj] = None # bpy.data.objects[obj]
for object_name in list(current[scene].keys()): # todo : exclude directly added/removed objects
#print("ob", object_name)
if object_name in previous[scene]:
# print("object", object_name,"in previous scene, comparing")
current_obj = current[scene][object_name]
prev_obj = previous[scene][object_name]
same = str(current_obj) == str(prev_obj)
if "Camera" in object_name:
pass#print(" current", current_obj, prev_obj)
"""if "Fox" in object_name:
print(" current", current_obj)
print(" previou", prev_obj)
print(" same?", same)"""
#print("foo", same)
if not same:
""" print(" current", current_obj)
print(" previou", prev_obj)"""
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][object_name] = bpy.data.objects[object_name]
bubble_up_changes(bpy.data.objects[object_name], changes_per_scene[scene])
# now bubble up for instances & parents
previous_stored.clear()
previous_stored.write(json.dumps(current))
print("changes per scene alternative", changes_per_scene)
return changes_per_scene
def execute(self, context):
bpy.context.window_manager.auto_export_tracker.disable_change_detection()
if self.direct_mode:
self.load_settings(context)
if self.will_save_settings:
self.save_settings(context)
#print("self", self.auto_export)
if self.auto_export: # only do the actual exporting if auto export is actually enabled
#changes_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene
#& do the export
if self.direct_mode: #Do not auto export when applying settings in the menu, do it on save only
# determine changed objects
changes_per_scene = self.did_objects_change()
# determine changed parameters
params_changed = self.did_export_settings_change()
auto_export(changes_per_scene, params_changed, self)
# cleanup
# reset the list of changes in the tracker
bpy.context.window_manager.auto_export_tracker.clear_changes()
print("AUTO EXPORT DONE")
bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=0.1)
else:
print("auto export disabled, skipping")
return {'FINISHED'}
def invoke(self, context, event):
#print("invoke")
bpy.context.window_manager.auto_export_tracker.disable_change_detection()
self.load_settings(context)
wm = context.window_manager
#wm.fileselect_add(self)
return context.window_manager.invoke_props_dialog(self, title="Auto export", width=640)
return {'RUNNING_MODAL'}
"""def modal(self, context, event):
if event.type == 'SPACE':
wm = context.window_manager
wm.invoke_popup(self)
#wm.invoke_props_dialog(self)
if event.type in {'ESC'}:
return {'CANCELLED'}
return {'RUNNING_MODAL'}"""
def draw(self, context):
layout = self.layout
operator = self
controls_enabled = self.auto_export
layout.prop(self, "auto_export")
layout.separator()
toggle_icon = "TRIA_DOWN" if self.show_general_settings else "TRIA_RIGHT"
layout.prop(self, "show_general_settings", text="General", icon=toggle_icon)
if self.show_general_settings:
section = layout.box()
section.enabled = controls_enabled
draw_folder_browser(section, "Export root folder", self.export_root_folder, "export_root_folder")
row = section.row()
draw_folder_browser(row, "Assets Folder (non blueprints mode only)", self.export_root_folder, "export_output_folder")
row.enabled = not self.export_blueprints
section.prop(operator, "export_blueprints")
section.prop(operator, "export_scene_settings")
"""header, panel = layout.panel("my_panel_id", default_closed=False)
header.label(text="Hello World")
if panel:
panel.label(text="Success")"""
toggle_icon = "TRIA_DOWN" if self.show_change_detection_settings else "TRIA_RIGHT"
layout.prop(operator, "show_change_detection_settings", text="Change Detection", icon=toggle_icon)
if self.show_change_detection_settings:
section = layout.box()
section.enabled = controls_enabled
section.prop(operator, "export_change_detection", text="Use change detection")
# main/level scenes
toggle_icon = "TRIA_DOWN" if self.show_scene_settings else "TRIA_RIGHT"
layout.prop(operator, "show_scene_settings", text="Scenes", icon=toggle_icon)
if self.show_scene_settings:
section = layout.box()
section.enabled = controls_enabled
rows = 2
row = section.row()
row.label(text="main scenes")
row.prop(context.window_manager, "main_scene", text='')
row = section.row()
row.template_list("SCENE_UL_GLTF_auto_export", "level scenes", operator, "main_scenes", operator, "main_scenes_index", rows=rows)
col = row.column(align=True)
sub_row = col.row()
add_operator = sub_row.operator("scene_list.list_action", icon='ADD', text="")
add_operator.action = 'ADD'
add_operator.scene_type = 'level'
#add_operator.operator = operator
sub_row.enabled = context.window_manager.main_scene is not None
sub_row = col.row()
remove_operator = sub_row.operator("scene_list.list_action", icon='REMOVE', text="")
remove_operator.action = 'REMOVE'
remove_operator.scene_type = 'level'
col.separator()
# library scenes
row = section.row()
row.label(text="library scenes")
row.prop(context.window_manager, "library_scene", text='')
row = section.row()
row.template_list("SCENE_UL_GLTF_auto_export", "library scenes", operator, "library_scenes", operator, "library_scenes_index", rows=rows)
col = row.column(align=True)
sub_row = col.row()
add_operator = sub_row.operator("scene_list.list_action", icon='ADD', text="")
add_operator.action = 'ADD'
add_operator.scene_type = 'library'
sub_row.enabled = context.window_manager.library_scene is not None
sub_row = col.row()
remove_operator = sub_row.operator("scene_list.list_action", icon='REMOVE', text="")
remove_operator.action = 'REMOVE'
remove_operator.scene_type = 'library'
col.separator()
toggle_icon = "TRIA_DOWN" if self.show_blueprint_settings else "TRIA_RIGHT"
layout.prop(operator, "show_blueprint_settings", text="Blueprints", icon=toggle_icon)
if self.show_blueprint_settings:
section = layout.box()
section.enabled = controls_enabled
section = section.box()
section.enabled = controls_enabled and self.export_blueprints
# collections/blueprints
draw_folder_browser(section, "Blueprints folder", self.export_root_folder, "export_blueprints_path")
#section.prop(operator, "export_blueprints_path")
section.prop(operator, "collection_instances_combine_mode")
section.prop(operator, "export_marked_assets")
section.separator()
draw_folder_browser(section, "Levels folder", self.export_root_folder, "export_levels_path")
#section.prop(operator, "export_levels_path")
section.prop(operator, "export_separate_dynamic_and_static_objects")
section.separator()
# materials
section.prop(operator, "export_materials_library")
section = section.box()
section.enabled = controls_enabled and self.export_materials_library
draw_folder_browser(section, 'Materials folder', self.export_root_folder, "export_materials_path")
#section.prop(operator, "export_materials_path")
def cancel(self, context):
print("cancel")
#bpy.context.window_manager.auto_export_tracker.enable_change_detection()
bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=1)

View File

@ -1,208 +0,0 @@
import os
from bpy.types import AddonPreferences
from bpy.props import (BoolProperty,
IntProperty,
StringProperty,
EnumProperty,
CollectionProperty
)
from .internals import (CUSTOM_PG_sceneName)
AutoExportGltfPreferenceNames = [
'will_save_settings',
'direct_mode',# specific to main auto_export operator
'show_general_settings',
'auto_export',
'export_root_folder',
'export_output_folder',
'export_scene_settings',
'show_change_detection_settings',
'export_change_detection',
'show_scene_settings',
'main_scenes',
'library_scenes',
'main_scenes_index',
'library_scenes_index',
'main_scene_names',
'library_scene_names',
'show_blueprint_settings',
'export_blueprints',
'export_blueprints_path',
'export_marked_assets',
'collection_instances_combine_mode',
'export_levels_path',
'export_separate_dynamic_and_static_objects',
'export_materials_library',
'export_materials_path',
]
def on_export_output_folder_updated(self, context):
#self.export_root_folder = os.path.relpath(self.export_root_folder)
#self.export_output_folder = os.path.join(self.export_root_folder, self.export_output_folder)
print("on_foo_updated", self.export_root_folder, self.export_output_folder)
class AutoExportGltfAddonPreferences(AddonPreferences):
# this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __package__
bl_options = {'PRESET'}
#### these are for the operator
will_save_settings: BoolProperty(
name='Remember Export Settings',
description='Store glTF export settings in the Blender project',
default=True
) # type: ignore
# use when operator is called directly, works a bit differently than inside the ui
direct_mode: BoolProperty(
default=False
) # type: ignore
auto_export: BoolProperty(
name='Auto export',
description='Automatically export to gltf on save',
default=False
) # type: ignore
#### general
# for UI only, workaround for lacking panels
show_general_settings: BoolProperty(
name="show_general settings",
description="show/hide general settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_root_folder: StringProperty(
name = "Project Root Path",
description="The root folder of your (Bevy) project (not assets!)",
# subtype='DIR_PATH',
default='../'
#update=on_export_output_folder_updated) # type: ignore
)
export_output_folder: StringProperty(
name='Export folder',
description='The root folder for all exports(relative to the root folder/path) Defaults to "assets" ',
default='./assets',
#subtype='DIR_PATH',
options={'HIDDEN'}
# update=on_export_output_folder_updated
) # type: ignore
# for UI only, workaround for lacking panels
show_change_detection_settings: BoolProperty(
name="show change detection settings",
description="show/hide change detection settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_change_detection: BoolProperty(
name='Change detection',
description='Use change detection to determine what/if should be exported',
default=True
) # type: ignore
# scenes
# for UI only, workaround for lacking panels
show_scene_settings: BoolProperty(
name="show scene settings",
description="show/hide scene settings (UI only: has no impact on exports)",
default=True
) # type: ignore
# scene components
export_scene_settings: BoolProperty(
name='Export scene settings',
description='Export scene settings ie AmbientLighting, Bloom, AO etc',
default=False
) # type: ignore
# blueprint settings
# for UI only, workaround for lacking panels
show_blueprint_settings: BoolProperty(
name="show blueprint settings",
description="show/hide blueprint settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property, and enabled a lot more features !',
default=True
) # type: ignore
export_blueprints_path: StringProperty(
name='Blueprints path',
description='path to export the blueprints to (relative to the assets folder)',
default='blueprints',
#subtype='DIR_PATH'
) # type: ignore
export_levels_path: StringProperty(
name='Levels path',
description='path to export the levels (main scenes) to (relative to the assets folder)',
default='levels',
#subtype='DIR_PATH'
) # type: ignore
export_separate_dynamic_and_static_objects: BoolProperty(
name="Export levels' dynamic and static objects seperatly",
description="""For MAIN scenes only (aka levels), toggle this to generate 2 files per level:
- one with all dynamic data: collection or instances marked as dynamic/ saveable
- one with all static data: anything else that is NOT marked as dynamic""",
default=False
) # type: ignore
export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',
default=False
) # type: ignore
export_materials_path: StringProperty(
name='Materials path',
description='path to export the materials libraries to (relative to the export folder)',
default='materials',
#subtype='DIR_PATH'
) # type: ignore
""" combine mode can be
- 'Split' (default): replace with an empty, creating links to sub blueprints
- 'Embed' : treat it as an embeded object and do not replace it with an empty
- 'EmbedExternal': embed any instance of a non local collection (ie external assets)
- 'Inject': inject components from sub collection instances into the curent object => this is now a seperate custom property that you can apply to a collecion instance
"""
collection_instances_combine_mode : EnumProperty(
name='Collection instances',
items=(
('Split', 'Split', 'replace collection instances with an empty + blueprint, creating links to sub blueprints (Default, Recomended)'),
('Embed', 'Embed', 'treat collection instances as embeded objects and do not replace them with an empty'),
('EmbedExternal', 'EmbedExternal', 'treat instances of external (not specifified in the current blend file) collections (aka assets etc) as embeded objects and do not replace them with empties'),
#('Inject', 'Inject', 'inject components from sub collection instances into the curent object')
),
default='Split'
) # type: ignore
export_marked_assets: BoolProperty(
name='Auto export marked assets',
description='Collections that have been marked as assets will be systematically exported, even if not in use in another scene',
default=True
) # type: ignore
main_scenes: CollectionProperty(name="main scenes", type=CUSTOM_PG_sceneName) # type: ignore
main_scenes_index: IntProperty(name = "Index for main scenes list", default = 0) # type: ignore
library_scenes: CollectionProperty(name="library scenes", type=CUSTOM_PG_sceneName) # type: ignore
library_scenes_index: IntProperty(name = "Index for library scenes list", default = 0) # type: ignore

View File

@ -1,195 +0,0 @@
import json
import bpy
from bpy.types import (PropertyGroup)
from bpy.props import (PointerProperty, IntProperty, StringProperty)
from .get_blueprints_to_export import get_blueprints_to_export
from ..constants import TEMPSCENE_PREFIX
from .internals import BlueprintsToExport
from ..helpers.helpers_scenes import (get_scenes)
from .preferences import AutoExportGltfAddonPreferences
class AutoExportTracker(PropertyGroup):
changed_objects_per_scene = {}
change_detection_enabled = True
export_params_changed = False
gltf_settings_backup = None
last_operator = None
dummy_file_path = ""
exports_total : IntProperty(
name='exports_total',
description='Number of total exports',
default=0
) # type: ignore
exports_count : IntProperty(
name='exports_count',
description='Number of exports in progress',
default=0
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.auto_export_tracker = PointerProperty(type=AutoExportTracker)
# register list of exportable collections
bpy.types.WindowManager.exportedCollections = bpy.props.CollectionProperty(type=BlueprintsToExport)
# setup handlers for updates & saving
#bpy.app.handlers.save_post.append(cls.save_handler)
#bpy.app.handlers.depsgraph_update_post.append(cls.deps_update_handler)
@classmethod
def unregister(cls):
# remove handlers & co
"""try:
bpy.app.handlers.depsgraph_update_post.remove(cls.deps_update_handler)
except:pass
try:
bpy.app.handlers.save_post.remove(cls.save_handler)
except:pass"""
del bpy.types.WindowManager.auto_export_tracker
del bpy.types.WindowManager.exportedCollections
@classmethod
def save_handler(cls, scene, depsgraph):
print("-------------")
print("saved", bpy.data.filepath)
# auto_export(changes_per_scene, export_parameters_changed)
bpy.ops.export_scenes.auto_gltf(direct_mode= True)
# (re)set a few things after exporting
# reset wether the gltf export paramters were changed since the last save
cls.export_params_changed = False
# reset whether there have been changed objects since the last save
cls.changed_objects_per_scene.clear()
# all our logic is done, mark this as done
@classmethod
def deps_post_update_handler(cls, scene, depsgraph):
# print("change detection enabled", cls.change_detection_enabled)
"""ops = bpy.context.window_manager.operators
print("last operators", ops)
for op in ops:
print("operator", op)"""
active_operator = bpy.context.active_operator
if active_operator:
#print("Operator", active_operator.bl_label, active_operator.bl_idname)
if active_operator.bl_idname == "EXPORT_SCENE_OT_gltf" and active_operator.gltf_export_id == "gltf_auto_export":
# we backup any existing gltf export settings, if there were any
scene = bpy.context.scene
if "glTF2ExportSettings" in scene:
existing_setting = scene["glTF2ExportSettings"]
bpy.context.window_manager.gltf_settings_backup = json.dumps(dict(existing_setting))
# we force saving params
active_operator.will_save_settings = True
# we set the last operator here so we can clear the specific settings (yeah for overly complex logic)
cls.last_operator = active_operator
#print("active_operator", active_operator.has_active_exporter_extensions, active_operator.__annotations__.keys(), active_operator.filepath, active_operator.gltf_export_id)
return
if active_operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf":
# we force saving params
active_operator.will_save_settings = True
active_operator.auto_export = True
# if we are using the operator, bail out for the rest
print("setting stuff for auto_export")
return
# only deal with changes if we are NOT in the mids of saving/exporting
if cls.change_detection_enabled:
# ignore anything going on with temporary scenes
if not scene.name.startswith(TEMPSCENE_PREFIX):
#print("depsgraph_update_post", scene.name)
changed_scene = scene.name or ""
#print("-------------")
if not changed_scene in cls.changed_objects_per_scene:
cls.changed_objects_per_scene[changed_scene] = {}
# print("cls.changed_objects_per_scene", cls.changed_objects_per_scene)
# depsgraph = bpy.context.evaluated_depsgraph_get()
for obj in depsgraph.updates:
#print("depsgraph update", obj)
if isinstance(obj.id, bpy.types.Object):
# get the actual object
object = bpy.data.objects[obj.id.name]
#print(" changed object", obj.id.name, "changes", obj, "evalutated", obj.id.is_evaluated, "transforms", obj.is_updated_transform, "geometry", obj.is_updated_geometry)
if obj.is_updated_transform or obj.is_updated_geometry:
cls.changed_objects_per_scene[scene.name][obj.id.name] = object
elif isinstance(obj.id, bpy.types.Material): # or isinstance(obj.id, bpy.types.ShaderNodeTree):
# print(" changed material", obj.id, "scene", scene.name,)
material = bpy.data.materials[obj.id.name]
#now find which objects are using the material
for obj in bpy.data.objects:
for slot in obj.material_slots:
if slot.material == material:
cls.changed_objects_per_scene[scene.name][obj.name] = obj
#print("changed_objects_per_scene", cls.changed_objects_per_scene)
"""for obj_name_original in cls.changed_objects_per_scene[scene_name]:
if obj_name_original != ls.changed_objects_per_scene[scene_name][obj_name_original]"""
items = 0
for scene_name in cls.changed_objects_per_scene:
items += len(cls.changed_objects_per_scene[scene_name].keys())
if items == 0:
cls.changed_objects_per_scene.clear()
#print("changed_objects_per_scene", cls.changed_objects_per_scene)
# filter out invalid objects
"""for scene_name in cls.changed_objects_per_scene.keys():
bla = {}
for object_name in cls.changed_objects_per_scene[scene.name]:
object = cls.changed_objects_per_scene[scene.name][object_name]"""
#print("sdfsd", object, object.valid)
#if not cls.changed_objects_per_scene[scene.name][object_name].invalid:
# bla[object_name] = cls.changed_objects_per_scene[scene.name][object_name]
#cls.changed_objects_per_scene[scene.name]= bla
#cls.changed_objects_per_scene[scene_name] = [o for o in cls.changed_objects_per_scene[scene_name] if not o.invalid]
# get a list of exportable collections for display
# keep it simple, just use Simplenamespace for compatibility with the rest of our code
# TODO: debounce
def disable_change_detection(self):
#print("disable change detection")
self.change_detection_enabled = False
self.__class__.change_detection_enabled = False
return None
def enable_change_detection(self):
#print("enable change detection")
self.change_detection_enabled = True
self.__class__.change_detection_enabled = True
#print("bpy.context.window_manager.auto_export_tracker.change_detection_enabled", bpy.context.window_manager.auto_export_tracker.change_detection_enabled)
return None
def clear_changes(self):
self.changed_objects_per_scene.clear()
self.__class__.changed_objects_per_scene.clear()
def export_finished(self):
#print("export_finished")
self.exports_count -= 1
if self.exports_count == 0:
print("preparing to reset change detection")
bpy.app.timers.register(self.enable_change_detection, first_interval=0.1)
#self.enable_change_detection()
return None
def get_auto_exporter_settings():
auto_exporter_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None
if auto_exporter_settings != None:
try:
auto_exporter_settings = json.loads(auto_exporter_settings.as_string())
except:
auto_exporter_settings = {}
else:
auto_exporter_settings = {}
return auto_exporter_settings

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