mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 22:50:19 +00:00
Merge branch 'main' into transmission
This commit is contained in:
commit
3862e82df5
171 changed files with 6519 additions and 1573 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -259,9 +259,9 @@ jobs:
|
|||
id: missing-metadata
|
||||
run: cargo run -p build-templated-pages -- check-missing examples
|
||||
- name: check for missing update
|
||||
id: missing-update
|
||||
run: cargo run -p build-templated-pages -- update examples
|
||||
- name: Check for modified files
|
||||
id: missing-update
|
||||
run: |
|
||||
echo "if this step fails, run the following command and commit the changed file on your PR."
|
||||
echo " > cargo run -p build-templated-pages -- update examples"
|
||||
|
@ -293,9 +293,9 @@ jobs:
|
|||
id: missing-features
|
||||
run: cargo run -p build-templated-pages -- check-missing features
|
||||
- name: check for missing update
|
||||
id: missing-update
|
||||
run: cargo run -p build-templated-pages -- update features
|
||||
- name: Check for modified files
|
||||
id: missing-update
|
||||
run: |
|
||||
echo "if this step fails, run the following command and commit the changed file on your PR."
|
||||
echo " > cargo run -p build-templated-pages -- update features"
|
||||
|
|
|
@ -265,6 +265,7 @@ Examples in Bevy should be:
|
|||
4. **Minimal:** They should be no larger or complex than is needed to meet the goals of the example.
|
||||
|
||||
When you add a new example, be sure to update `examples/README.md` with the new example and add it to the root `Cargo.toml` file.
|
||||
Run `cargo run -p build-templated-pages -- build-example-page` to do this automatically.
|
||||
Use a generous sprinkling of keywords in your description: these are commonly used to search for a specific example.
|
||||
See the [example style guide](.github/contributing/example_style_guide.md) to help make sure the style of your example matches what we're already using.
|
||||
|
||||
|
|
|
@ -25,3 +25,4 @@
|
|||
* Low poly fox [by PixelMannen](https://opengameart.org/content/fox-and-shiba) (CC0 1.0 Universal)
|
||||
* Rigging and animation [by @tomkranis on Sketchfab](https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc) ([CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/))
|
||||
* FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE)
|
||||
* Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0)
|
||||
|
|
90
Cargo.toml
90
Cargo.toml
|
@ -52,6 +52,7 @@ default = [
|
|||
"bevy_gizmos",
|
||||
"android_shared_stdcxx",
|
||||
"tonemapping_luts",
|
||||
"default_font",
|
||||
]
|
||||
|
||||
# Force dynamic linking, which improves iterative compile times
|
||||
|
@ -108,6 +109,9 @@ trace_chrome = ["trace", "bevy_internal/trace_chrome"]
|
|||
# Tracing support, exposing a port for Tracy
|
||||
trace_tracy = ["trace", "bevy_internal/trace_tracy"]
|
||||
|
||||
# Tracing support, with memory profiling, exposing a port for Tracy
|
||||
trace_tracy_memory = ["trace", "bevy_internal/trace_tracy", "bevy_internal/trace_tracy_memory"]
|
||||
|
||||
# Tracing support
|
||||
trace = ["bevy_internal/trace"]
|
||||
|
||||
|
@ -222,6 +226,9 @@ accesskit_unix = ["bevy_internal/accesskit_unix"]
|
|||
# Enable assertions to check the validity of parameters passed to glam
|
||||
glam_assert = ["bevy_internal/glam_assert"]
|
||||
|
||||
# Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase
|
||||
default_font = ["bevy_internal/default_font"]
|
||||
|
||||
[dependencies]
|
||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.11.0-dev", default-features = false, optional = true }
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false }
|
||||
|
@ -319,6 +326,16 @@ description = "Renders a rectangle, circle, and hexagon"
|
|||
category = "2D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_gltf_vertex_attribute"
|
||||
path = "examples/2d/custom_gltf_vertex_attribute.rs"
|
||||
|
||||
[package.metadata.example.custom_gltf_vertex_attribute]
|
||||
name = "Custom glTF vertex attribute 2D"
|
||||
description = "Renders a glTF mesh in 2D with a custom vertex attribute"
|
||||
category = "2D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "2d_gizmos"
|
||||
path = "examples/2d/2d_gizmos.rs"
|
||||
|
@ -729,6 +746,16 @@ description = "Create and play an animation defined by code that operates on the
|
|||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "cubic_curve"
|
||||
path = "examples/animation/cubic_curve.rs"
|
||||
|
||||
[package.metadata.example.cubic_curve]
|
||||
name = "Cubic Curve"
|
||||
description = "Bezier curve example showing a cube following a cubic curve"
|
||||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_skinned_mesh"
|
||||
path = "examples/animation/custom_skinned_mesh.rs"
|
||||
|
@ -1257,6 +1284,16 @@ description = "Iterates and prints gamepad input and connection events"
|
|||
category = "Input"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "gamepad_rumble"
|
||||
path = "examples/input/gamepad_rumble.rs"
|
||||
|
||||
[package.metadata.example.gamepad_rumble]
|
||||
name = "Gamepad Rumble"
|
||||
description = "Shows how to rumble a gamepad using force feedback"
|
||||
category = "Input"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "keyboard_input"
|
||||
path = "examples/input/keyboard_input.rs"
|
||||
|
@ -1755,6 +1792,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "overflow"
|
||||
path = "examples/ui/overflow.rs"
|
||||
|
||||
[package.metadata.example.overflow]
|
||||
name = "Overflow"
|
||||
description = "Simple example demonstrating overflow behavior"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "overflow_debug"
|
||||
path = "examples/ui/overflow_debug.rs"
|
||||
|
@ -1775,6 +1822,16 @@ description = "Showcases the RelativeCursorPosition component"
|
|||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "size_constraints"
|
||||
path = "examples/ui/size_constraints.rs"
|
||||
|
||||
[package.metadata.example.size_constraints]
|
||||
name = "Size Constraints"
|
||||
description = "Demonstrates how the to use the size constraints to control the size of a UI node."
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "text"
|
||||
path = "examples/ui/text.rs"
|
||||
|
@ -1803,7 +1860,28 @@ path = "examples/ui/flex_layout.rs"
|
|||
name = "Flex Layout"
|
||||
description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text"
|
||||
category = "UI (User Interface)"
|
||||
wasm = false
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "text_wrap_debug"
|
||||
path = "examples/ui/text_wrap_debug.rs"
|
||||
|
||||
[package.metadata.example.text_wrap_debug]
|
||||
name = "Text Wrap Debug"
|
||||
description = "Demonstrates text wrapping"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "grid"
|
||||
path = "examples/ui/grid.rs"
|
||||
|
||||
[package.metadata.example.grid]
|
||||
name = "CSS Grid"
|
||||
description = "An example for CSS Grid layout"
|
||||
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "transparency_ui"
|
||||
|
@ -1886,6 +1964,16 @@ description = "Illustrates how to customize the default window settings"
|
|||
category = "Window"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "screenshot"
|
||||
path = "examples/window/screenshot.rs"
|
||||
|
||||
[package.metadata.example.screenshot]
|
||||
name = "Screenshot"
|
||||
description = "Shows how to save screenshots to disk"
|
||||
category = "Window"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "transparent_window"
|
||||
path = "examples/window/transparent_window.rs"
|
||||
|
|
80
assets/models/barycentric/barycentric.gltf
Normal file
80
assets/models/barycentric/barycentric.gltf
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"byteOffset": 0,
|
||||
"count": 4,
|
||||
"componentType": 5126,
|
||||
"type": "VEC3",
|
||||
"min": [
|
||||
-1.0,
|
||||
-1.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
1.0,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"bufferView": 0,
|
||||
"byteOffset": 12,
|
||||
"count": 4,
|
||||
"componentType": 5126,
|
||||
"type": "VEC4"
|
||||
},
|
||||
{
|
||||
"bufferView": 0,
|
||||
"byteOffset": 28,
|
||||
"count": 4,
|
||||
"componentType": 5126,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"byteOffset": 0,
|
||||
"count": 6,
|
||||
"componentType": 5123,
|
||||
"type": "SCALAR"
|
||||
}
|
||||
],
|
||||
"asset": {
|
||||
"version": "2.0"
|
||||
},
|
||||
"buffers": [
|
||||
{
|
||||
"byteLength": 172,
|
||||
"uri": "data:application/gltf-buffer;base64,AACAvwAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAIC/AAAAAAAAAD8AAAA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIC/AACAPwAAAAAAAAA/AAAAPwAAAAAAAIA/AAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAQACAAIAAQADAA=="
|
||||
}
|
||||
],
|
||||
"bufferViews": [
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 160,
|
||||
"byteOffset": 0,
|
||||
"byteStride": 40,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 12,
|
||||
"byteOffset": 160,
|
||||
"target": 34962
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"COLOR_0": 1,
|
||||
"__BARYCENTRIC": 2
|
||||
},
|
||||
"indices": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
36
assets/shaders/custom_gltf_2d.wgsl
Normal file
36
assets/shaders/custom_gltf_2d.wgsl
Normal file
|
@ -0,0 +1,36 @@
|
|||
#import bevy_sprite::mesh2d_view_bindings
|
||||
#import bevy_sprite::mesh2d_bindings
|
||||
#import bevy_sprite::mesh2d_functions
|
||||
|
||||
struct Vertex {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) barycentric: vec3<f32>,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) barycentric: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertex(vertex: Vertex) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
|
||||
out.color = vertex.color;
|
||||
out.barycentric = vertex.barycentric;
|
||||
return out;
|
||||
}
|
||||
|
||||
struct FragmentInput {
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) barycentric: vec3<f32>,
|
||||
};
|
||||
|
||||
@fragment
|
||||
fn fragment(input: FragmentInput) -> @location(0) vec4<f32> {
|
||||
let d = min(input.barycentric.x, min(input.barycentric.y, input.barycentric.z));
|
||||
let t = 0.05 * (0.85 + sin(5.0 * globals.time));
|
||||
return mix(vec4(1.0,1.0,1.0,1.0), input.color, smoothstep(t, t+0.01, d));
|
||||
}
|
|
@ -9,6 +9,9 @@ criterion_group!(
|
|||
benches,
|
||||
concrete_struct_apply,
|
||||
concrete_struct_field,
|
||||
concrete_struct_type_info,
|
||||
concrete_struct_clone,
|
||||
dynamic_struct_clone,
|
||||
dynamic_struct_apply,
|
||||
dynamic_struct_get_field,
|
||||
dynamic_struct_insert,
|
||||
|
@ -110,6 +113,128 @@ fn concrete_struct_apply(criterion: &mut Criterion) {
|
|||
}
|
||||
}
|
||||
|
||||
fn concrete_struct_type_info(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("concrete_struct_type_info");
|
||||
group.warm_up_time(WARM_UP_TIME);
|
||||
group.measurement_time(MEASUREMENT_TIME);
|
||||
|
||||
let structs: [(Box<dyn Struct>, Box<dyn Struct>); 5] = [
|
||||
(
|
||||
Box::new(Struct1::default()),
|
||||
Box::new(GenericStruct1::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct16::default()),
|
||||
Box::new(GenericStruct16::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct32::default()),
|
||||
Box::new(GenericStruct32::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct64::default()),
|
||||
Box::new(GenericStruct64::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct128::default()),
|
||||
Box::new(GenericStruct128::<u32>::default()),
|
||||
),
|
||||
];
|
||||
|
||||
for (standard, generic) in structs {
|
||||
let field_count = standard.field_len();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("NonGeneric", field_count),
|
||||
&standard,
|
||||
|bencher, s| {
|
||||
bencher.iter(|| black_box(s.get_type_info()));
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Generic", field_count),
|
||||
&generic,
|
||||
|bencher, s| {
|
||||
bencher.iter(|| black_box(s.get_type_info()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn concrete_struct_clone(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("concrete_struct_clone");
|
||||
group.warm_up_time(WARM_UP_TIME);
|
||||
group.measurement_time(MEASUREMENT_TIME);
|
||||
|
||||
let structs: [(Box<dyn Struct>, Box<dyn Struct>); 5] = [
|
||||
(
|
||||
Box::new(Struct1::default()),
|
||||
Box::new(GenericStruct1::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct16::default()),
|
||||
Box::new(GenericStruct16::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct32::default()),
|
||||
Box::new(GenericStruct32::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct64::default()),
|
||||
Box::new(GenericStruct64::<u32>::default()),
|
||||
),
|
||||
(
|
||||
Box::new(Struct128::default()),
|
||||
Box::new(GenericStruct128::<u32>::default()),
|
||||
),
|
||||
];
|
||||
|
||||
for (standard, generic) in structs {
|
||||
let field_count = standard.field_len();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("NonGeneric", field_count),
|
||||
&standard,
|
||||
|bencher, s| {
|
||||
bencher.iter(|| black_box(s.clone_dynamic()));
|
||||
},
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Generic", field_count),
|
||||
&generic,
|
||||
|bencher, s| {
|
||||
bencher.iter(|| black_box(s.clone_dynamic()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dynamic_struct_clone(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("dynamic_struct_clone");
|
||||
group.warm_up_time(WARM_UP_TIME);
|
||||
group.measurement_time(MEASUREMENT_TIME);
|
||||
|
||||
let structs: [Box<dyn Struct>; 5] = [
|
||||
Box::new(Struct1::default().clone_dynamic()),
|
||||
Box::new(Struct16::default().clone_dynamic()),
|
||||
Box::new(Struct32::default().clone_dynamic()),
|
||||
Box::new(Struct64::default().clone_dynamic()),
|
||||
Box::new(Struct128::default().clone_dynamic()),
|
||||
];
|
||||
|
||||
for s in structs {
|
||||
let field_count = s.field_len();
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(field_count),
|
||||
&s,
|
||||
|bencher, s| {
|
||||
bencher.iter(|| black_box(s.clone_dynamic()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dynamic_struct_apply(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("dynamic_struct_apply");
|
||||
group.warm_up_time(WARM_UP_TIME);
|
||||
|
@ -228,6 +353,11 @@ fn dynamic_struct_get_field(criterion: &mut Criterion) {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct Struct1 {
|
||||
field_0: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct Struct16 {
|
||||
field_0: u32,
|
||||
|
@ -483,3 +613,264 @@ struct Struct128 {
|
|||
field_126: u32,
|
||||
field_127: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct GenericStruct1<T: Reflect + Default> {
|
||||
field_0: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct GenericStruct16<T: Reflect + Default> {
|
||||
field_0: T,
|
||||
field_1: T,
|
||||
field_2: T,
|
||||
field_3: T,
|
||||
field_4: T,
|
||||
field_5: T,
|
||||
field_6: T,
|
||||
field_7: T,
|
||||
field_8: T,
|
||||
field_9: T,
|
||||
field_10: T,
|
||||
field_11: T,
|
||||
field_12: T,
|
||||
field_13: T,
|
||||
field_14: T,
|
||||
field_15: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct GenericStruct32<T: Reflect + Default> {
|
||||
field_0: T,
|
||||
field_1: T,
|
||||
field_2: T,
|
||||
field_3: T,
|
||||
field_4: T,
|
||||
field_5: T,
|
||||
field_6: T,
|
||||
field_7: T,
|
||||
field_8: T,
|
||||
field_9: T,
|
||||
field_10: T,
|
||||
field_11: T,
|
||||
field_12: T,
|
||||
field_13: T,
|
||||
field_14: T,
|
||||
field_15: T,
|
||||
field_16: T,
|
||||
field_17: T,
|
||||
field_18: T,
|
||||
field_19: T,
|
||||
field_20: T,
|
||||
field_21: T,
|
||||
field_22: T,
|
||||
field_23: T,
|
||||
field_24: T,
|
||||
field_25: T,
|
||||
field_26: T,
|
||||
field_27: T,
|
||||
field_28: T,
|
||||
field_29: T,
|
||||
field_30: T,
|
||||
field_31: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct GenericStruct64<T: Reflect + Default> {
|
||||
field_0: T,
|
||||
field_1: T,
|
||||
field_2: T,
|
||||
field_3: T,
|
||||
field_4: T,
|
||||
field_5: T,
|
||||
field_6: T,
|
||||
field_7: T,
|
||||
field_8: T,
|
||||
field_9: T,
|
||||
field_10: T,
|
||||
field_11: T,
|
||||
field_12: T,
|
||||
field_13: T,
|
||||
field_14: T,
|
||||
field_15: T,
|
||||
field_16: T,
|
||||
field_17: T,
|
||||
field_18: T,
|
||||
field_19: T,
|
||||
field_20: T,
|
||||
field_21: T,
|
||||
field_22: T,
|
||||
field_23: T,
|
||||
field_24: T,
|
||||
field_25: T,
|
||||
field_26: T,
|
||||
field_27: T,
|
||||
field_28: T,
|
||||
field_29: T,
|
||||
field_30: T,
|
||||
field_31: T,
|
||||
field_32: T,
|
||||
field_33: T,
|
||||
field_34: T,
|
||||
field_35: T,
|
||||
field_36: T,
|
||||
field_37: T,
|
||||
field_38: T,
|
||||
field_39: T,
|
||||
field_40: T,
|
||||
field_41: T,
|
||||
field_42: T,
|
||||
field_43: T,
|
||||
field_44: T,
|
||||
field_45: T,
|
||||
field_46: T,
|
||||
field_47: T,
|
||||
field_48: T,
|
||||
field_49: T,
|
||||
field_50: T,
|
||||
field_51: T,
|
||||
field_52: T,
|
||||
field_53: T,
|
||||
field_54: T,
|
||||
field_55: T,
|
||||
field_56: T,
|
||||
field_57: T,
|
||||
field_58: T,
|
||||
field_59: T,
|
||||
field_60: T,
|
||||
field_61: T,
|
||||
field_62: T,
|
||||
field_63: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Reflect)]
|
||||
struct GenericStruct128<T: Reflect + Default> {
|
||||
field_0: T,
|
||||
field_1: T,
|
||||
field_2: T,
|
||||
field_3: T,
|
||||
field_4: T,
|
||||
field_5: T,
|
||||
field_6: T,
|
||||
field_7: T,
|
||||
field_8: T,
|
||||
field_9: T,
|
||||
field_10: T,
|
||||
field_11: T,
|
||||
field_12: T,
|
||||
field_13: T,
|
||||
field_14: T,
|
||||
field_15: T,
|
||||
field_16: T,
|
||||
field_17: T,
|
||||
field_18: T,
|
||||
field_19: T,
|
||||
field_20: T,
|
||||
field_21: T,
|
||||
field_22: T,
|
||||
field_23: T,
|
||||
field_24: T,
|
||||
field_25: T,
|
||||
field_26: T,
|
||||
field_27: T,
|
||||
field_28: T,
|
||||
field_29: T,
|
||||
field_30: T,
|
||||
field_31: T,
|
||||
field_32: T,
|
||||
field_33: T,
|
||||
field_34: T,
|
||||
field_35: T,
|
||||
field_36: T,
|
||||
field_37: T,
|
||||
field_38: T,
|
||||
field_39: T,
|
||||
field_40: T,
|
||||
field_41: T,
|
||||
field_42: T,
|
||||
field_43: T,
|
||||
field_44: T,
|
||||
field_45: T,
|
||||
field_46: T,
|
||||
field_47: T,
|
||||
field_48: T,
|
||||
field_49: T,
|
||||
field_50: T,
|
||||
field_51: T,
|
||||
field_52: T,
|
||||
field_53: T,
|
||||
field_54: T,
|
||||
field_55: T,
|
||||
field_56: T,
|
||||
field_57: T,
|
||||
field_58: T,
|
||||
field_59: T,
|
||||
field_60: T,
|
||||
field_61: T,
|
||||
field_62: T,
|
||||
field_63: T,
|
||||
field_64: T,
|
||||
field_65: T,
|
||||
field_66: T,
|
||||
field_67: T,
|
||||
field_68: T,
|
||||
field_69: T,
|
||||
field_70: T,
|
||||
field_71: T,
|
||||
field_72: T,
|
||||
field_73: T,
|
||||
field_74: T,
|
||||
field_75: T,
|
||||
field_76: T,
|
||||
field_77: T,
|
||||
field_78: T,
|
||||
field_79: T,
|
||||
field_80: T,
|
||||
field_81: T,
|
||||
field_82: T,
|
||||
field_83: T,
|
||||
field_84: T,
|
||||
field_85: T,
|
||||
field_86: T,
|
||||
field_87: T,
|
||||
field_88: T,
|
||||
field_89: T,
|
||||
field_90: T,
|
||||
field_91: T,
|
||||
field_92: T,
|
||||
field_93: T,
|
||||
field_94: T,
|
||||
field_95: T,
|
||||
field_96: T,
|
||||
field_97: T,
|
||||
field_98: T,
|
||||
field_99: T,
|
||||
field_100: T,
|
||||
field_101: T,
|
||||
field_102: T,
|
||||
field_103: T,
|
||||
field_104: T,
|
||||
field_105: T,
|
||||
field_106: T,
|
||||
field_107: T,
|
||||
field_108: T,
|
||||
field_109: T,
|
||||
field_110: T,
|
||||
field_111: T,
|
||||
field_112: T,
|
||||
field_113: T,
|
||||
field_114: T,
|
||||
field_115: T,
|
||||
field_116: T,
|
||||
field_117: T,
|
||||
field_118: T,
|
||||
field_119: T,
|
||||
field_120: T,
|
||||
field_121: T,
|
||||
field_122: T,
|
||||
field_123: T,
|
||||
field_124: T,
|
||||
field_125: T,
|
||||
field_126: T,
|
||||
field_127: T,
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ fn find_bone(
|
|||
Some(current_entity)
|
||||
}
|
||||
|
||||
/// Verify that there are no ancestors of a given entity that have an `AnimationPlayer`.
|
||||
/// Verify that there are no ancestors of a given entity that have an [`AnimationPlayer`].
|
||||
fn verify_no_ancestor_player(
|
||||
player_parent: Option<&Parent>,
|
||||
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
|
||||
|
|
|
@ -160,11 +160,9 @@ impl SubApp {
|
|||
}
|
||||
}
|
||||
|
||||
/// Runs the `SubApp`'s default schedule.
|
||||
/// Runs the [`SubApp`]'s default schedule.
|
||||
pub fn run(&mut self) {
|
||||
self.app
|
||||
.world
|
||||
.run_schedule_ref(&*self.app.main_schedule_label);
|
||||
self.app.world.run_schedule(&*self.app.main_schedule_label);
|
||||
self.app.world.clear_trackers();
|
||||
}
|
||||
|
||||
|
@ -241,7 +239,7 @@ impl App {
|
|||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _bevy_frame_update_span = info_span!("main app").entered();
|
||||
self.world.run_schedule_ref(&*self.main_schedule_label);
|
||||
self.world.run_schedule(&*self.main_schedule_label);
|
||||
}
|
||||
for (_label, sub_app) in self.sub_apps.iter_mut() {
|
||||
#[cfg(feature = "trace")]
|
||||
|
@ -674,7 +672,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
/// Boxed variant of `add_plugin`, can be used from a [`PluginGroup`]
|
||||
/// Boxed variant of [`add_plugin`](App::add_plugin) that can be used from a [`PluginGroup`]
|
||||
pub(crate) fn add_boxed_plugin(
|
||||
&mut self,
|
||||
plugin: Box<dyn Plugin>,
|
||||
|
|
|
@ -143,7 +143,7 @@ impl Main {
|
|||
|
||||
world.resource_scope(|world, order: Mut<MainScheduleOrder>| {
|
||||
for label in &order.labels {
|
||||
let _ = world.try_run_schedule_ref(&**label);
|
||||
let _ = world.try_run_schedule(&**label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use parking_lot::{Mutex, RwLock};
|
|||
use std::{path::Path, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that occur while loading assets with an `AssetServer`.
|
||||
/// Errors that occur while loading assets with an [`AssetServer`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetServerError {
|
||||
/// Asset folder is not a directory.
|
||||
|
|
|
@ -276,7 +276,7 @@ pub trait AddAsset {
|
|||
where
|
||||
T: Asset;
|
||||
|
||||
/// Registers the asset type `T` using `[App::register]`,
|
||||
/// Registers the asset type `T` using [`App::register_type`],
|
||||
/// and adds [`ReflectAsset`] type data to `T` and [`ReflectHandle`] type data to [`Handle<T>`] in the type registry.
|
||||
///
|
||||
/// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`].
|
||||
|
@ -296,7 +296,7 @@ pub trait AddAsset {
|
|||
|
||||
/// Adds an asset loader `T` using default values.
|
||||
///
|
||||
/// The default values may come from the `World` or from `T::default()`.
|
||||
/// The default values may come from the [`World`] or from `T::default()`.
|
||||
fn init_asset_loader<T>(&mut self) -> &mut Self
|
||||
where
|
||||
T: AssetLoader + FromWorld;
|
||||
|
@ -306,7 +306,7 @@ pub trait AddAsset {
|
|||
/// Internal assets (e.g. shaders) are bundled directly into the app and can't be hot reloaded
|
||||
/// using the conventional API. See `DebugAssetServerPlugin`.
|
||||
///
|
||||
/// The default values may come from the `World` or from `T::default()`.
|
||||
/// The default values may come from the [`World`] or from `T::default()`.
|
||||
fn init_debug_asset_loader<T>(&mut self) -> &mut Self
|
||||
where
|
||||
T: AssetLoader + FromWorld;
|
||||
|
@ -407,7 +407,7 @@ impl AddAsset for App {
|
|||
/// Loads an internal asset.
|
||||
///
|
||||
/// Internal assets (e.g. shaders) are bundled directly into the app and can't be hot reloaded
|
||||
/// using the conventional API. See `DebugAssetServerPlugin`.
|
||||
/// using the conventional API. See [`DebugAssetServerPlugin`](crate::debug_asset_server::DebugAssetServerPlugin).
|
||||
#[cfg(feature = "debug_asset_server")]
|
||||
#[macro_export]
|
||||
macro_rules! load_internal_asset {
|
||||
|
|
|
@ -11,9 +11,9 @@ use std::{
|
|||
///
|
||||
/// Implementation details:
|
||||
///
|
||||
/// - `load_path` uses the [AssetManager] to load files.
|
||||
/// - `read_directory` always returns an empty iterator.
|
||||
/// - `get_metadata` will probably return an error.
|
||||
/// - [`load_path`](AssetIo::load_path) uses the [`AssetManager`] to load files.
|
||||
/// - [`read_directory`](AssetIo::read_directory) always returns an empty iterator.
|
||||
/// - [`get_metadata`](AssetIo::get_metadata) will probably return an error.
|
||||
/// - Watching for changes is not supported. The watcher methods will do nothing.
|
||||
///
|
||||
/// [AssetManager]: https://developer.android.com/reference/android/content/res/AssetManager
|
||||
|
|
|
@ -82,7 +82,7 @@ impl FileAssetIo {
|
|||
|
||||
/// Returns the root directory where assets are loaded from.
|
||||
///
|
||||
/// See `get_base_path`.
|
||||
/// See [`get_base_path`](FileAssetIo::get_base_path).
|
||||
pub fn root_path(&self) -> &PathBuf {
|
||||
&self.root_path
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ pub trait AssetIo: Downcast + Send + Sync + 'static {
|
|||
|
||||
/// Tells the asset I/O to watch for changes recursively at the provided path.
|
||||
///
|
||||
/// No-op if `watch_for_changes` hasn't been called yet.
|
||||
/// No-op if [`watch_for_changes`](AssetIo::watch_for_changes) hasn't been called yet.
|
||||
/// Otherwise triggers a reload each time `to_watch` changes.
|
||||
/// In most cases the asset found at the watched path should be changed,
|
||||
/// but when an asset depends on data at another path, the asset's path
|
||||
|
|
|
@ -80,7 +80,7 @@ impl Default for AssetPlugin {
|
|||
}
|
||||
|
||||
impl AssetPlugin {
|
||||
/// Creates an instance of the platform's default `AssetIo`.
|
||||
/// Creates an instance of the platform's default [`AssetIo`].
|
||||
///
|
||||
/// This is useful when providing a custom `AssetIo` instance that needs to
|
||||
/// delegate to the default `AssetIo` for the platform.
|
||||
|
|
|
@ -7,7 +7,8 @@ use crate::{Asset, Assets, Handle, HandleId, HandleUntyped};
|
|||
|
||||
/// Type data for the [`TypeRegistry`](bevy_reflect::TypeRegistry) used to operate on reflected [`Asset`]s.
|
||||
///
|
||||
/// This type provides similar methods to [`Assets<T>`] like `get`, `add` and `remove`, but can be used in situations where you don't know which asset type `T` you want
|
||||
/// This type provides similar methods to [`Assets<T>`] like [`get`](ReflectAsset::get),
|
||||
/// [`add`](ReflectAsset::add) and [`remove`](ReflectAsset::remove), but can be used in situations where you don't know which asset type `T` you want
|
||||
/// until runtime.
|
||||
///
|
||||
/// [`ReflectAsset`] can be obtained via [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered using [`register_asset_reflect`](crate::AddAsset::register_asset_reflect).
|
||||
|
|
|
@ -34,7 +34,7 @@ use std::path::PathBuf;
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
||||
|
||||
/// Registration of default types to the `TypeRegistry` resource.
|
||||
/// Registration of default types to the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource.
|
||||
#[derive(Default)]
|
||||
pub struct TypeRegistrationPlugin;
|
||||
|
||||
|
@ -95,10 +95,12 @@ fn register_math_types(app: &mut App) {
|
|||
.register_type::<bevy_math::Mat3A>()
|
||||
.register_type::<bevy_math::Mat4>()
|
||||
.register_type::<bevy_math::DQuat>()
|
||||
.register_type::<bevy_math::Quat>();
|
||||
.register_type::<bevy_math::Quat>()
|
||||
.register_type::<bevy_math::Rect>();
|
||||
}
|
||||
|
||||
/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`, `IoTaskPool`.
|
||||
/// Setup of default task pools: [`AsyncComputeTaskPool`](bevy_tasks::AsyncComputeTaskPool),
|
||||
/// [`ComputeTaskPool`](bevy_tasks::ComputeTaskPool), [`IoTaskPool`](bevy_tasks::IoTaskPool).
|
||||
#[derive(Default)]
|
||||
pub struct TaskPoolPlugin {
|
||||
/// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at application start.
|
||||
|
|
|
@ -61,10 +61,10 @@ pub struct MotionVectorPrepass;
|
|||
#[derive(Component)]
|
||||
pub struct ViewPrepassTextures {
|
||||
/// The depth texture generated by the prepass.
|
||||
/// Exists only if [`DepthPrepass`] is added to the `ViewTarget`
|
||||
/// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
|
||||
pub depth: Option<CachedTexture>,
|
||||
/// The normals texture generated by the prepass.
|
||||
/// Exists only if [`NormalPrepass`] is added to the `ViewTarget`
|
||||
/// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
|
||||
pub normal: Option<CachedTexture>,
|
||||
/// The motion vectors texture generated by the prepass.
|
||||
/// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget`
|
||||
|
|
|
@ -148,7 +148,7 @@ impl Entity {
|
|||
/// // ... replace the entities with valid ones.
|
||||
/// ```
|
||||
///
|
||||
/// Deriving `Reflect` for a component that has an `Entity` field:
|
||||
/// Deriving [`Reflect`](bevy_reflect::Reflect) for a component that has an `Entity` field:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use bevy_ecs::{prelude::*, component::*};
|
||||
|
@ -301,15 +301,15 @@ pub struct Entities {
|
|||
/// that have been freed or are in the process of being allocated:
|
||||
///
|
||||
/// - The `freelist` IDs, previously freed by `free()`. These IDs are available to any of
|
||||
/// `alloc()`, `reserve_entity()` or `reserve_entities()`. Allocation will always prefer
|
||||
/// [`alloc`], [`reserve_entity`] or [`reserve_entities`]. Allocation will always prefer
|
||||
/// these over brand new IDs.
|
||||
///
|
||||
/// - The `reserved` list of IDs that were once in the freelist, but got reserved by
|
||||
/// `reserve_entities` or `reserve_entity()`. They are now waiting for `flush()` to make them
|
||||
/// [`reserve_entities`] or [`reserve_entity`]. They are now waiting for [`flush`] to make them
|
||||
/// fully allocated.
|
||||
///
|
||||
/// - The count of new IDs that do not yet exist in `self.meta`, but which we have handed out
|
||||
/// and reserved. `flush()` will allocate room for them in `self.meta`.
|
||||
/// and reserved. [`flush`] will allocate room for them in `self.meta`.
|
||||
///
|
||||
/// The contents of `pending` look like this:
|
||||
///
|
||||
|
@ -331,7 +331,12 @@ pub struct Entities {
|
|||
/// This formulation allows us to reserve any number of IDs first from the freelist
|
||||
/// and then from the new IDs, using only a single atomic subtract.
|
||||
///
|
||||
/// Once `flush()` is done, `free_cursor` will equal `pending.len()`.
|
||||
/// Once [`flush`] is done, `free_cursor` will equal `pending.len()`.
|
||||
///
|
||||
/// [`alloc`]: Entities::alloc
|
||||
/// [`reserve_entity`]: Entities::reserve_entity
|
||||
/// [`reserve_entities`]: Entities::reserve_entities
|
||||
/// [`flush`]: Entities::flush
|
||||
pending: Vec<u32>,
|
||||
free_cursor: AtomicIdCursor,
|
||||
/// Stores the number of free entities for [`len`](Entities::len)
|
||||
|
@ -350,7 +355,7 @@ impl Entities {
|
|||
|
||||
/// Reserve entity IDs concurrently.
|
||||
///
|
||||
/// Storage for entity generation and location is lazily allocated by calling `flush`.
|
||||
/// Storage for entity generation and location is lazily allocated by calling [`flush`](Entities::flush).
|
||||
pub fn reserve_entities(&self, count: u32) -> ReserveEntitiesIterator {
|
||||
// Use one atomic subtract to grab a range of new IDs. The range might be
|
||||
// entirely nonnegative, meaning all IDs come from the freelist, or entirely
|
||||
|
@ -626,8 +631,8 @@ impl Entities {
|
|||
*self.free_cursor.get_mut() != self.pending.len() as IdCursor
|
||||
}
|
||||
|
||||
/// Allocates space for entities previously reserved with `reserve_entity` or
|
||||
/// `reserve_entities`, then initializes each one using the supplied function.
|
||||
/// Allocates space for entities previously reserved with [`reserve_entity`](Entities::reserve_entity) or
|
||||
/// [`reserve_entities`](Entities::reserve_entities), then initializes each one using the supplied function.
|
||||
///
|
||||
/// # Safety
|
||||
/// Flush _must_ set the entity location to the correct [`ArchetypeId`] for the given [`Entity`]
|
||||
|
|
|
@ -46,8 +46,8 @@ pub mod prelude {
|
|||
system::{
|
||||
adapter as system_adapter,
|
||||
adapter::{dbg, error, ignore, info, unwrap, warn},
|
||||
Commands, Deferred, In, IntoPipeSystem, IntoSystem, Local, NonSend, NonSendMut,
|
||||
ParallelCommands, ParamSet, Query, Res, ResMut, Resource, System, SystemParamFunction,
|
||||
Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands,
|
||||
ParamSet, Query, Res, ResMut, Resource, System, SystemParamFunction,
|
||||
},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ pub mod prelude {
|
|||
|
||||
pub use bevy_utils::all_tuples;
|
||||
|
||||
/// A specialized hashmap type with Key of `TypeId`
|
||||
/// A specialized hashmap type with Key of [`TypeId`]
|
||||
type TypeIdMap<V> = rustc_hash::FxHashMap<TypeId, V>;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -25,6 +25,7 @@ struct FormattedBitSet<'a, T: SparseSetIndex> {
|
|||
bit_set: &'a FixedBitSet,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
|
||||
fn new(bit_set: &'a FixedBitSet) -> Self {
|
||||
Self {
|
||||
|
@ -33,6 +34,7 @@ impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
|
@ -69,6 +71,7 @@ impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for Access<T> {
|
|||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SparseSetIndex> Default for Access<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
@ -144,7 +147,7 @@ impl<T: SparseSetIndex> Access<T> {
|
|||
|
||||
/// Returns `true` if the access and `other` can be active at the same time.
|
||||
///
|
||||
/// `Access` instances are incompatible if one can write
|
||||
/// [`Access`] instances are incompatible if one can write
|
||||
/// an element that the other can read or write.
|
||||
pub fn is_compatible(&self, other: &Access<T>) -> bool {
|
||||
// Only systems that do not write data are compatible with systems that operate on `&World`.
|
||||
|
@ -213,31 +216,22 @@ impl<T: SparseSetIndex> Access<T> {
|
|||
/// is read/write `T`, read `U`. It must still have a read `U` access otherwise the following
|
||||
/// queries would be incorrectly considered disjoint:
|
||||
/// - `Query<&mut T>` read/write `T`
|
||||
/// - `Query<Option<&T>` accesses nothing
|
||||
/// - `Query<Option<&T>>` accesses nothing
|
||||
///
|
||||
/// See comments the `WorldQuery` impls of `AnyOf`/`Option`/`Or` for more information.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
/// See comments the [`WorldQuery`](super::WorldQuery) impls of [`AnyOf`](super::AnyOf)/`Option`/[`Or`](super::Or) for more information.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct FilteredAccess<T: SparseSetIndex> {
|
||||
access: Access<T>,
|
||||
with: FixedBitSet,
|
||||
without: FixedBitSet,
|
||||
}
|
||||
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for FilteredAccess<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FilteredAccess")
|
||||
.field("access", &self.access)
|
||||
.field("with", &FormattedBitSet::<T>::new(&self.with))
|
||||
.field("without", &FormattedBitSet::<T>::new(&self.without))
|
||||
.finish()
|
||||
}
|
||||
// An array of filter sets to express `With` or `Without` clauses in disjunctive normal form, for example: `Or<(With<A>, With<B>)>`.
|
||||
// Filters like `(With<A>, Or<(With<B>, Without<C>)>` are expanded into `Or<((With<A>, With<B>), (With<A>, Without<C>))>`.
|
||||
filter_sets: Vec<AccessFilters<T>>,
|
||||
}
|
||||
|
||||
impl<T: SparseSetIndex> Default for FilteredAccess<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
access: Access::default(),
|
||||
with: Default::default(),
|
||||
without: Default::default(),
|
||||
filter_sets: vec![AccessFilters::default()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -266,30 +260,46 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
|
|||
/// Adds access to the element given by `index`.
|
||||
pub fn add_read(&mut self, index: T) {
|
||||
self.access.add_read(index.clone());
|
||||
self.add_with(index);
|
||||
self.and_with(index);
|
||||
}
|
||||
|
||||
/// Adds exclusive access to the element given by `index`.
|
||||
pub fn add_write(&mut self, index: T) {
|
||||
self.access.add_write(index.clone());
|
||||
self.add_with(index);
|
||||
self.and_with(index);
|
||||
}
|
||||
|
||||
/// Retains only combinations where the element given by `index` is also present.
|
||||
pub fn add_with(&mut self, index: T) {
|
||||
self.with.grow(index.sparse_set_index() + 1);
|
||||
self.with.insert(index.sparse_set_index());
|
||||
/// Adds a `With` filter: corresponds to a conjunction (AND) operation.
|
||||
///
|
||||
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
|
||||
/// Adding `AND With<C>` via this method transforms it into the equivalent of `Or<((With<A>, With<C>), (With<B>, With<C>))>`.
|
||||
pub fn and_with(&mut self, index: T) {
|
||||
let index = index.sparse_set_index();
|
||||
for filter in &mut self.filter_sets {
|
||||
filter.with.grow(index + 1);
|
||||
filter.with.insert(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retains only combinations where the element given by `index` is not present.
|
||||
pub fn add_without(&mut self, index: T) {
|
||||
self.without.grow(index.sparse_set_index() + 1);
|
||||
self.without.insert(index.sparse_set_index());
|
||||
/// Adds a `Without` filter: corresponds to a conjunction (AND) operation.
|
||||
///
|
||||
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
|
||||
/// Adding `AND Without<C>` via this method transforms it into the equivalent of `Or<((With<A>, Without<C>), (With<B>, Without<C>))>`.
|
||||
pub fn and_without(&mut self, index: T) {
|
||||
let index = index.sparse_set_index();
|
||||
for filter in &mut self.filter_sets {
|
||||
filter.without.grow(index + 1);
|
||||
filter.without.insert(index);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_intersect_filter(&mut self, other: &FilteredAccess<T>) {
|
||||
self.without.intersect_with(&other.without);
|
||||
self.with.intersect_with(&other.with);
|
||||
/// Appends an array of filters: corresponds to a disjunction (OR) operation.
|
||||
///
|
||||
/// As the underlying array of filters represents a disjunction,
|
||||
/// where each element (`AccessFilters`) represents a conjunction,
|
||||
/// we can simply append to the array.
|
||||
pub fn append_or(&mut self, other: &FilteredAccess<T>) {
|
||||
self.filter_sets.append(&mut other.filter_sets.clone());
|
||||
}
|
||||
|
||||
pub fn extend_access(&mut self, other: &FilteredAccess<T>) {
|
||||
|
@ -298,9 +308,23 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
|
|||
|
||||
/// Returns `true` if this and `other` can be active at the same time.
|
||||
pub fn is_compatible(&self, other: &FilteredAccess<T>) -> bool {
|
||||
self.access.is_compatible(&other.access)
|
||||
|| !self.with.is_disjoint(&other.without)
|
||||
|| !other.with.is_disjoint(&self.without)
|
||||
if self.access.is_compatible(&other.access) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the access instances are incompatible, we want to check that whether filters can
|
||||
// guarantee that queries are disjoint.
|
||||
// Since the `filter_sets` array represents a Disjunctive Normal Form formula ("ORs of ANDs"),
|
||||
// we need to make sure that each filter set (ANDs) rule out every filter set from the `other` instance.
|
||||
//
|
||||
// For example, `Query<&mut C, Or<(With<A>, Without<B>)>>` is compatible `Query<&mut C, (With<B>, Without<A>)>`,
|
||||
// but `Query<&mut C, Or<(Without<A>, Without<B>)>>` isn't compatible with `Query<&mut C, Or<(With<A>, With<B>)>>`.
|
||||
self.filter_sets.iter().all(|filter| {
|
||||
other
|
||||
.filter_sets
|
||||
.iter()
|
||||
.all(|other_filter| filter.is_ruled_out_by(other_filter))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a vector of elements that this and `other` cannot access at the same time.
|
||||
|
@ -313,10 +337,34 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
|
|||
}
|
||||
|
||||
/// Adds all access and filters from `other`.
|
||||
pub fn extend(&mut self, access: &FilteredAccess<T>) {
|
||||
self.access.extend(&access.access);
|
||||
self.with.union_with(&access.with);
|
||||
self.without.union_with(&access.without);
|
||||
///
|
||||
/// Corresponds to a conjunction operation (AND) for filters.
|
||||
///
|
||||
/// Extending `Or<(With<A>, Without<B>)>` with `Or<(With<C>, Without<D>)>` will result in
|
||||
/// `Or<((With<A>, With<C>), (With<A>, Without<D>), (Without<B>, With<C>), (Without<B>, Without<D>))>`.
|
||||
pub fn extend(&mut self, other: &FilteredAccess<T>) {
|
||||
self.access.extend(&other.access);
|
||||
|
||||
// We can avoid allocating a new array of bitsets if `other` contains just a single set of filters:
|
||||
// in this case we can short-circuit by performing an in-place union for each bitset.
|
||||
if other.filter_sets.len() == 1 {
|
||||
for filter in &mut self.filter_sets {
|
||||
filter.with.union_with(&other.filter_sets[0].with);
|
||||
filter.without.union_with(&other.filter_sets[0].without);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new_filters = Vec::with_capacity(self.filter_sets.len() * other.filter_sets.len());
|
||||
for filter in &self.filter_sets {
|
||||
for other_filter in &other.filter_sets {
|
||||
let mut new_filter = filter.clone();
|
||||
new_filter.with.union_with(&other_filter.with);
|
||||
new_filter.without.union_with(&other_filter.without);
|
||||
new_filters.push(new_filter);
|
||||
}
|
||||
}
|
||||
self.filter_sets = new_filters;
|
||||
}
|
||||
|
||||
/// Sets the underlying unfiltered access as having access to all indexed elements.
|
||||
|
@ -325,6 +373,43 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
struct AccessFilters<T> {
|
||||
with: FixedBitSet,
|
||||
without: FixedBitSet,
|
||||
_index_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for AccessFilters<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AccessFilters")
|
||||
.field("with", &FormattedBitSet::<T>::new(&self.with))
|
||||
.field("without", &FormattedBitSet::<T>::new(&self.without))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SparseSetIndex> Default for AccessFilters<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
with: FixedBitSet::default(),
|
||||
without: FixedBitSet::default(),
|
||||
_index_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SparseSetIndex> AccessFilters<T> {
|
||||
fn is_ruled_out_by(&self, other: &Self) -> bool {
|
||||
// Although not technically complete, we don't consider the case when `AccessFilters`'s
|
||||
// `without` bitset contradicts its own `with` bitset (e.g. `(With<A>, Without<A>)`).
|
||||
// Such query would be considered compatible with any other query, but as it's almost
|
||||
// always an error, we ignore this case instead of treating such query as compatible
|
||||
// with others.
|
||||
!self.with.is_disjoint(&other.without) || !self.without.is_disjoint(&other.with)
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of [`FilteredAccess`] instances.
|
||||
///
|
||||
/// Used internally to statically check if systems have conflicting access.
|
||||
|
@ -353,7 +438,7 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
|
|||
/// compatible.
|
||||
/// 2. A "fine grained" check, it kicks in when the "coarse" check fails.
|
||||
/// the two access sets might still be compatible if some of the accesses
|
||||
/// are restricted with the `With` or `Without` filters so that access is
|
||||
/// are restricted with the [`With`](super::With) or [`Without`](super::Without) filters so that access is
|
||||
/// mutually exclusive. The fine grained phase iterates over all filters in
|
||||
/// the `self` set and compares it to all the filters in the `other` set,
|
||||
/// making sure they are all mutually compatible.
|
||||
|
@ -441,7 +526,10 @@ impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::query::access::AccessFilters;
|
||||
use crate::query::{Access, FilteredAccess, FilteredAccessSet};
|
||||
use fixedbitset::FixedBitSet;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[test]
|
||||
fn read_all_access_conflicts() {
|
||||
|
@ -514,22 +602,67 @@ mod tests {
|
|||
let mut access_a = FilteredAccess::<usize>::default();
|
||||
access_a.add_read(0);
|
||||
access_a.add_read(1);
|
||||
access_a.add_with(2);
|
||||
access_a.and_with(2);
|
||||
|
||||
let mut access_b = FilteredAccess::<usize>::default();
|
||||
access_b.add_read(0);
|
||||
access_b.add_write(3);
|
||||
access_b.add_without(4);
|
||||
access_b.and_without(4);
|
||||
|
||||
access_a.extend(&access_b);
|
||||
|
||||
let mut expected = FilteredAccess::<usize>::default();
|
||||
expected.add_read(0);
|
||||
expected.add_read(1);
|
||||
expected.add_with(2);
|
||||
expected.and_with(2);
|
||||
expected.add_write(3);
|
||||
expected.add_without(4);
|
||||
expected.and_without(4);
|
||||
|
||||
assert!(access_a.eq(&expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_access_extend_or() {
|
||||
let mut access_a = FilteredAccess::<usize>::default();
|
||||
// Exclusive access to `(&mut A, &mut B)`.
|
||||
access_a.add_write(0);
|
||||
access_a.add_write(1);
|
||||
|
||||
// Filter by `With<C>`.
|
||||
let mut access_b = FilteredAccess::<usize>::default();
|
||||
access_b.and_with(2);
|
||||
|
||||
// Filter by `(With<D>, Without<E>)`.
|
||||
let mut access_c = FilteredAccess::<usize>::default();
|
||||
access_c.and_with(3);
|
||||
access_c.and_without(4);
|
||||
|
||||
// Turns `access_b` into `Or<(With<C>, (With<D>, Without<D>))>`.
|
||||
access_b.append_or(&access_c);
|
||||
// Applies the filters to the initial query, which corresponds to the FilteredAccess'
|
||||
// representation of `Query<(&mut A, &mut B), Or<(With<C>, (With<D>, Without<E>))>>`.
|
||||
access_a.extend(&access_b);
|
||||
|
||||
// Construct the expected `FilteredAccess` struct.
|
||||
// The intention here is to test that exclusive access implied by `add_write`
|
||||
// forms correct normalized access structs when extended with `Or` filters.
|
||||
let mut expected = FilteredAccess::<usize>::default();
|
||||
expected.add_write(0);
|
||||
expected.add_write(1);
|
||||
// The resulted access is expected to represent `Or<((With<A>, With<B>, With<C>), (With<A>, With<B>, With<D>, Without<E>))>`.
|
||||
expected.filter_sets = vec![
|
||||
AccessFilters {
|
||||
with: FixedBitSet::with_capacity_and_blocks(3, [0b111]),
|
||||
without: FixedBitSet::default(),
|
||||
_index_type: PhantomData,
|
||||
},
|
||||
AccessFilters {
|
||||
with: FixedBitSet::with_capacity_and_blocks(4, [0b1011]),
|
||||
without: FixedBitSet::with_capacity_and_blocks(5, [0b10000]),
|
||||
_index_type: PhantomData,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(access_a, expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1157,8 +1157,8 @@ macro_rules! impl_tuple_fetch {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn filter_fetch<'w>(
|
||||
_fetch: &mut Self::Fetch<'w>,
|
||||
unsafe fn filter_fetch(
|
||||
_fetch: &mut Self::Fetch<'_>,
|
||||
_entity: Entity,
|
||||
_table_row: TableRow
|
||||
) -> bool {
|
||||
|
@ -1281,34 +1281,21 @@ macro_rules! impl_anytuple_fetch {
|
|||
fn update_component_access(state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {
|
||||
let ($($name,)*) = state;
|
||||
|
||||
// We do not unconditionally add `$name`'s `with`/`without` accesses to `_access`
|
||||
// as this would be unsound. For example the following two queries should conflict:
|
||||
// - Query<(AnyOf<(&A, ())>, &mut B)>
|
||||
// - Query<&mut B, Without<A>>
|
||||
//
|
||||
// If we were to unconditionally add `$name`'s `with`/`without` accesses then `AnyOf<(&A, ())>`
|
||||
// would have a `With<A>` access which is incorrect as this `WorldQuery` will match entities that
|
||||
// do not have the `A` component. This is the same logic as the `Or<...>: WorldQuery` impl.
|
||||
//
|
||||
// The correct thing to do here is to only add a `with`/`without` access to `_access` if all
|
||||
// `$name` params have that `with`/`without` access. More jargony put- we add the intersection
|
||||
// of all `with`/`without` accesses of the `$name` params to `_access`.
|
||||
let mut _intersected_access = _access.clone();
|
||||
let mut _new_access = _access.clone();
|
||||
let mut _not_first = false;
|
||||
$(
|
||||
if _not_first {
|
||||
let mut intermediate = _access.clone();
|
||||
$name::update_component_access($name, &mut intermediate);
|
||||
_intersected_access.extend_intersect_filter(&intermediate);
|
||||
_intersected_access.extend_access(&intermediate);
|
||||
_new_access.append_or(&intermediate);
|
||||
_new_access.extend_access(&intermediate);
|
||||
} else {
|
||||
|
||||
$name::update_component_access($name, &mut _intersected_access);
|
||||
$name::update_component_access($name, &mut _new_access);
|
||||
_not_first = true;
|
||||
}
|
||||
)*
|
||||
|
||||
*_access = _intersected_access;
|
||||
*_access = _new_access;
|
||||
}
|
||||
|
||||
fn update_archetype_component_access(state: &Self::State, _archetype: &Archetype, _access: &mut Access<ArchetypeComponentId>) {
|
||||
|
|
|
@ -86,7 +86,7 @@ unsafe impl<T: Component> WorldQuery for With<T> {
|
|||
|
||||
#[inline]
|
||||
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess<ComponentId>) {
|
||||
access.add_with(id);
|
||||
access.and_with(id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -183,7 +183,7 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
|
|||
|
||||
#[inline]
|
||||
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess<ComponentId>) {
|
||||
access.add_without(id);
|
||||
access.and_without(id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -328,8 +328,8 @@ macro_rules! impl_query_filter_tuple {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn filter_fetch<'w>(
|
||||
fetch: &mut Self::Fetch<'w>,
|
||||
unsafe fn filter_fetch(
|
||||
fetch: &mut Self::Fetch<'_>,
|
||||
entity: Entity,
|
||||
table_row: TableRow
|
||||
) -> bool {
|
||||
|
@ -339,33 +339,21 @@ macro_rules! impl_query_filter_tuple {
|
|||
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>) {
|
||||
let ($($filter,)*) = state;
|
||||
|
||||
// We do not unconditionally add `$filter`'s `with`/`without` accesses to `access`
|
||||
// as this would be unsound. For example the following two queries should conflict:
|
||||
// - Query<&mut B, Or<(With<A>, ())>>
|
||||
// - Query<&mut B, Without<A>>
|
||||
//
|
||||
// If we were to unconditionally add `$name`'s `with`/`without` accesses then `Or<(With<A>, ())>`
|
||||
// would have a `With<A>` access which is incorrect as this `WorldQuery` will match entities that
|
||||
// do not have the `A` component. This is the same logic as the `AnyOf<...>: WorldQuery` impl.
|
||||
//
|
||||
// The correct thing to do here is to only add a `with`/`without` access to `_access` if all
|
||||
// `$filter` params have that `with`/`without` access. More jargony put- we add the intersection
|
||||
// of all `with`/`without` accesses of the `$filter` params to `access`.
|
||||
let mut _intersected_access = access.clone();
|
||||
let mut _new_access = access.clone();
|
||||
let mut _not_first = false;
|
||||
$(
|
||||
if _not_first {
|
||||
let mut intermediate = access.clone();
|
||||
$filter::update_component_access($filter, &mut intermediate);
|
||||
_intersected_access.extend_intersect_filter(&intermediate);
|
||||
_intersected_access.extend_access(&intermediate);
|
||||
_new_access.append_or(&intermediate);
|
||||
_new_access.extend_access(&intermediate);
|
||||
} else {
|
||||
$filter::update_component_access($filter, &mut _intersected_access);
|
||||
$filter::update_component_access($filter, &mut _new_access);
|
||||
_not_first = true;
|
||||
}
|
||||
)*
|
||||
|
||||
*access = _intersected_access;
|
||||
*access = _new_access;
|
||||
}
|
||||
|
||||
fn update_archetype_component_access(state: &Self::State, archetype: &Archetype, access: &mut Access<ArchetypeComponentId>) {
|
||||
|
@ -516,8 +504,8 @@ macro_rules! impl_tick_filter {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn filter_fetch<'w>(
|
||||
fetch: &mut Self::Fetch<'w>,
|
||||
unsafe fn filter_fetch(
|
||||
fetch: &mut Self::Fetch<'_>,
|
||||
entity: Entity,
|
||||
table_row: TableRow
|
||||
) -> bool {
|
||||
|
|
|
@ -143,6 +143,7 @@ pub mod common_conditions {
|
|||
change_detection::DetectChanges,
|
||||
event::{Event, EventReader},
|
||||
prelude::{Component, Query, With},
|
||||
removal_detection::RemovedComponents,
|
||||
schedule::{State, States},
|
||||
system::{IntoSystem, Res, Resource, System},
|
||||
};
|
||||
|
@ -893,6 +894,17 @@ pub mod common_conditions {
|
|||
move |query: Query<(), With<T>>| !query.is_empty()
|
||||
}
|
||||
|
||||
/// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true`
|
||||
/// if there are any entity with a component of the given type removed.
|
||||
pub fn any_component_removed<T: Component>() -> impl FnMut(RemovedComponents<T>) -> bool {
|
||||
// `RemovedComponents` based on events and therefore events need to be consumed,
|
||||
// so that there are no false positives on subsequent calls of the run condition.
|
||||
// Simply checking `is_empty` would not be enough.
|
||||
// PERF: note that `count` is efficient (not actually looping/iterating),
|
||||
// due to Bevy having a specialized implementation for events.
|
||||
move |mut removals: RemovedComponents<T>| !removals.iter().count() != 0
|
||||
}
|
||||
|
||||
/// Generates a [`Condition`](super::Condition) that inverses the result of passed one.
|
||||
///
|
||||
/// # Example
|
||||
|
|
|
@ -47,7 +47,7 @@ pub enum ExecutorKind {
|
|||
/// (along with dependency information for multi-threaded execution).
|
||||
///
|
||||
/// Since the arrays are sorted in the same order, elements are referenced by their index.
|
||||
/// `FixedBitSet` is used as a smaller, more efficient substitute of `HashSet<usize>`.
|
||||
/// [`FixedBitSet`] is used as a smaller, more efficient substitute of `HashSet<usize>`.
|
||||
#[derive(Default)]
|
||||
pub struct SystemSchedule {
|
||||
pub(super) systems: Vec<BoxedSystem>,
|
||||
|
|
|
@ -56,7 +56,7 @@ impl SyncUnsafeSchedule<'_> {
|
|||
/// Per-system data used by the [`MultiThreadedExecutor`].
|
||||
// Copied here because it can't be read from the system when it's running.
|
||||
struct SystemTaskMetadata {
|
||||
/// The `ArchetypeComponentId` access of the system.
|
||||
/// The [`ArchetypeComponentId`] access of the system.
|
||||
archetype_component_access: Access<ArchetypeComponentId>,
|
||||
/// Indices of the systems that directly depend on the system.
|
||||
dependents: Vec<usize>,
|
||||
|
@ -320,6 +320,8 @@ impl MultiThreadedExecutor {
|
|||
|
||||
self.ready_systems.set(system_index, false);
|
||||
|
||||
// SAFETY: Since `self.can_run` returned true earlier, it must have called
|
||||
// `update_archetype_component_access` for each run condition.
|
||||
if !self.should_run(system_index, system, conditions, world) {
|
||||
self.skip_system_and_signal_dependents(system_index);
|
||||
continue;
|
||||
|
@ -338,7 +340,9 @@ impl MultiThreadedExecutor {
|
|||
break;
|
||||
}
|
||||
|
||||
// SAFETY: No other reference to this system exists.
|
||||
// SAFETY:
|
||||
// - No other reference to this system exists.
|
||||
// - `self.can_run` has been called, which calls `update_archetype_component_access` with this system.
|
||||
unsafe {
|
||||
self.spawn_system_task(scope, system_index, systems, world);
|
||||
}
|
||||
|
@ -408,7 +412,11 @@ impl MultiThreadedExecutor {
|
|||
true
|
||||
}
|
||||
|
||||
fn should_run(
|
||||
/// # Safety
|
||||
///
|
||||
/// `update_archetype_component` must have been called with `world`
|
||||
/// for each run condition in `conditions`.
|
||||
unsafe fn should_run(
|
||||
&mut self,
|
||||
system_index: usize,
|
||||
_system: &BoxedSystem,
|
||||
|
@ -421,7 +429,8 @@ impl MultiThreadedExecutor {
|
|||
continue;
|
||||
}
|
||||
|
||||
// evaluate system set's conditions
|
||||
// Evaluate the system set's conditions.
|
||||
// SAFETY: `update_archetype_component_access` has been called for each run condition.
|
||||
let set_conditions_met =
|
||||
evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world);
|
||||
|
||||
|
@ -434,7 +443,8 @@ impl MultiThreadedExecutor {
|
|||
self.evaluated_sets.insert(set_idx);
|
||||
}
|
||||
|
||||
// evaluate system's conditions
|
||||
// Evaluate the system's conditions.
|
||||
// SAFETY: `update_archetype_component_access` has been called for each run condition.
|
||||
let system_conditions_met =
|
||||
evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world);
|
||||
|
||||
|
@ -448,7 +458,9 @@ impl MultiThreadedExecutor {
|
|||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must not alias systems that are running.
|
||||
/// - Caller must not alias systems that are running.
|
||||
/// - `update_archetype_component_access` must have been called with `world`
|
||||
/// on the system assocaited with `system_index`.
|
||||
unsafe fn spawn_system_task<'scope>(
|
||||
&mut self,
|
||||
scope: &Scope<'_, 'scope, ()>,
|
||||
|
@ -470,7 +482,9 @@ impl MultiThreadedExecutor {
|
|||
#[cfg(feature = "trace")]
|
||||
let system_guard = system_span.enter();
|
||||
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
// SAFETY: access is compatible
|
||||
// SAFETY:
|
||||
// - Access: TODO.
|
||||
// - `update_archetype_component_access` has been called.
|
||||
unsafe { system.run_unsafe((), world) };
|
||||
}));
|
||||
#[cfg(feature = "trace")]
|
||||
|
@ -673,7 +687,11 @@ fn apply_system_buffers(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool {
|
||||
/// # Safety
|
||||
///
|
||||
/// `update_archetype_component_access` must have been called
|
||||
/// with `world` for each condition in `conditions`.
|
||||
unsafe fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool {
|
||||
// not short-circuiting is intentional
|
||||
#[allow(clippy::unnecessary_fold)]
|
||||
conditions
|
||||
|
|
|
@ -172,3 +172,40 @@ where
|
|||
SystemTypeSet::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
schedule::{tests::ResMut, Schedule},
|
||||
system::Resource,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_boxed_label() {
|
||||
use crate::{self as bevy_ecs, world::World};
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Flag(bool);
|
||||
|
||||
#[derive(ScheduleLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct A;
|
||||
|
||||
let mut world = World::new();
|
||||
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.add_systems(|mut flag: ResMut<Flag>| flag.0 = true);
|
||||
world.add_schedule(schedule, A);
|
||||
|
||||
let boxed: Box<dyn ScheduleLabel> = Box::new(A);
|
||||
|
||||
world.insert_resource(Flag(false));
|
||||
world.run_schedule(&boxed);
|
||||
assert!(world.resource::<Flag>().0);
|
||||
|
||||
world.insert_resource(Flag(false));
|
||||
world.run_schedule(boxed);
|
||||
assert!(world.resource::<Flag>().0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use bevy_utils::OnDrop;
|
|||
/// A flat, type-erased data storage type
|
||||
///
|
||||
/// Used to densely store homogeneous ECS data. A blob is usually just an arbitrary block of contiguous memory without any identity, and
|
||||
/// could be used to represent any arbitrary data (i.e. string, arrays, etc). This type is an extendable and reallcatable blob, which makes
|
||||
/// could be used to represent any arbitrary data (i.e. string, arrays, etc). This type is an extendable and re-allocatable blob, which makes
|
||||
/// it a blobby Vec, a `BlobVec`.
|
||||
pub(super) struct BlobVec {
|
||||
item_layout: Layout,
|
||||
|
|
|
@ -259,7 +259,7 @@ impl<const SEND: bool> Resources<SEND> {
|
|||
///
|
||||
/// # Panics
|
||||
/// Will panic if `component_id` is not valid for the provided `components`
|
||||
/// If `SEND` is false, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
|
||||
/// If `SEND` is true, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
|
||||
pub(crate) fn initialize_with(
|
||||
&mut self,
|
||||
component_id: ComponentId,
|
||||
|
@ -269,7 +269,11 @@ impl<const SEND: bool> Resources<SEND> {
|
|||
self.resources.get_or_insert_with(component_id, || {
|
||||
let component_info = components.get_info(component_id).unwrap();
|
||||
if SEND {
|
||||
assert!(component_info.is_send_and_sync());
|
||||
assert!(
|
||||
component_info.is_send_and_sync(),
|
||||
"Send + Sync resource {} initialized as non_send. It may have been inserted via World::insert_non_send_resource by accident. Try using World::insert_resource instead.",
|
||||
component_info.name(),
|
||||
);
|
||||
}
|
||||
ResourceData {
|
||||
column: ManuallyDrop::new(Column::with_capacity(component_info, 1)),
|
||||
|
|
|
@ -722,7 +722,7 @@ mod tests {
|
|||
);
|
||||
|
||||
fn init_component<T: Component>(sets: &mut SparseSets, id: usize) {
|
||||
let descriptor = ComponentDescriptor::new::<TestComponent1>();
|
||||
let descriptor = ComponentDescriptor::new::<T>();
|
||||
let id = ComponentId::new(id);
|
||||
let info = ComponentInfo::new(id, descriptor);
|
||||
sets.get_or_insert(&info);
|
||||
|
|
|
@ -164,6 +164,8 @@ where
|
|||
// so the caller will guarantee that no other systems will conflict with `a` or `b`.
|
||||
// Since these closures are `!Send + !Sync + !'static`, they can never be called
|
||||
// in parallel, so their world accesses will not conflict with each other.
|
||||
// Additionally, `update_archetype_component_access` has been called,
|
||||
// which forwards to the implementations for `self.a` and `self.b`.
|
||||
|input| self.a.run_unsafe(input, world),
|
||||
|input| self.b.run_unsafe(input, world),
|
||||
)
|
||||
|
@ -235,3 +237,65 @@ where
|
|||
B: ReadOnlySystem,
|
||||
{
|
||||
}
|
||||
|
||||
/// A [`System`] created by piping the output of the first system into the input of the second.
|
||||
///
|
||||
/// This can be repeated indefinitely, but system pipes cannot branch: the output is consumed by the receiving system.
|
||||
///
|
||||
/// Given two systems `A` and `B`, A may be piped into `B` as `A.pipe(B)` if the output type of `A` is
|
||||
/// equal to the input type of `B`.
|
||||
///
|
||||
/// Note that for [`FunctionSystem`](crate::system::FunctionSystem)s the output is the return value
|
||||
/// of the function and the input is the first [`SystemParam`](crate::system::SystemParam) if it is
|
||||
/// tagged with [`In`](crate::system::In) or `()` if the function has no designated input parameter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::ParseIntError;
|
||||
///
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut world = World::default();
|
||||
/// world.insert_resource(Message("42".to_string()));
|
||||
///
|
||||
/// // pipe the `parse_message_system`'s output into the `filter_system`s input
|
||||
/// let mut piped_system = parse_message_system.pipe(filter_system);
|
||||
/// piped_system.initialize(&mut world);
|
||||
/// assert_eq!(piped_system.run((), &mut world), Some(42));
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Resource)]
|
||||
/// struct Message(String);
|
||||
///
|
||||
/// fn parse_message_system(message: Res<Message>) -> Result<usize, ParseIntError> {
|
||||
/// message.0.parse::<usize>()
|
||||
/// }
|
||||
///
|
||||
/// fn filter_system(In(result): In<Result<usize, ParseIntError>>) -> Option<usize> {
|
||||
/// result.ok().filter(|&n| n < 100)
|
||||
/// }
|
||||
/// ```
|
||||
pub type PipeSystem<SystemA, SystemB> = CombinatorSystem<Pipe, SystemA, SystemB>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct Pipe;
|
||||
|
||||
impl<A, B> Combine<A, B> for Pipe
|
||||
where
|
||||
A: System,
|
||||
B: System<In = A::Out>,
|
||||
{
|
||||
type In = A::In;
|
||||
type Out = B::Out;
|
||||
|
||||
fn combine(
|
||||
input: Self::In,
|
||||
a: impl FnOnce(A::In) -> A::Out,
|
||||
b: impl FnOnce(B::In) -> B::Out,
|
||||
) -> Self::Out {
|
||||
let value = a(input);
|
||||
b(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ impl<'w, 's> Commands<'w, 's> {
|
|||
/// Pushes a [`Command`] to the queue for creating a new [`Entity`] if the given one does not exists,
|
||||
/// and returns its corresponding [`EntityCommands`].
|
||||
///
|
||||
/// This method silently fails by returning `EntityCommands`
|
||||
/// This method silently fails by returning [`EntityCommands`]
|
||||
/// even if the given `Entity` cannot be spawned.
|
||||
///
|
||||
/// See [`World::get_or_spawn`] for more details.
|
||||
|
@ -345,7 +345,7 @@ impl<'w, 's> Commands<'w, 's> {
|
|||
|
||||
/// Pushes a [`Command`] to the queue for creating entities with a particular [`Bundle`] type.
|
||||
///
|
||||
/// `bundles_iter` is a type that can be converted into a `Bundle` iterator
|
||||
/// `bundles_iter` is a type that can be converted into a [`Bundle`] iterator
|
||||
/// (it can also be a collection).
|
||||
///
|
||||
/// This method is equivalent to iterating `bundles_iter`
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, In, IntoSystem,
|
||||
System, SystemMeta,
|
||||
},
|
||||
world::{World, WorldId},
|
||||
world::World,
|
||||
};
|
||||
|
||||
use bevy_utils::all_tuples;
|
||||
|
@ -25,7 +25,6 @@ where
|
|||
func: F,
|
||||
param_state: Option<<F::Param as ExclusiveSystemParam>::State>,
|
||||
system_meta: SystemMeta,
|
||||
world_id: Option<WorldId>,
|
||||
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
||||
marker: PhantomData<fn() -> Marker>,
|
||||
}
|
||||
|
@ -43,7 +42,6 @@ where
|
|||
func,
|
||||
param_state: None,
|
||||
system_meta: SystemMeta::new::<F>(),
|
||||
world_id: None,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +130,6 @@ where
|
|||
|
||||
#[inline]
|
||||
fn initialize(&mut self, world: &mut World) {
|
||||
self.world_id = Some(world.id());
|
||||
self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX);
|
||||
self.param_state = Some(F::Param::init(world, &mut self.system_meta));
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
use bevy_utils::all_tuples;
|
||||
use std::{any::TypeId, borrow::Cow, marker::PhantomData};
|
||||
|
||||
use super::ReadOnlySystem;
|
||||
use super::{In, IntoSystem, ReadOnlySystem};
|
||||
|
||||
/// The metadata of a [`System`].
|
||||
#[derive(Clone)]
|
||||
|
@ -308,65 +308,6 @@ impl<Param: SystemParam> FromWorld for SystemState<Param> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Conversion trait to turn something into a [`System`].
|
||||
///
|
||||
/// Use this to get a system from a function. Also note that every system implements this trait as
|
||||
/// well.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn my_system_function(a_usize_local: Local<usize>) {}
|
||||
///
|
||||
/// let system = IntoSystem::into_system(my_system_function);
|
||||
/// ```
|
||||
// This trait has to be generic because we have potentially overlapping impls, in particular
|
||||
// because Rust thinks a type could impl multiple different `FnMut` combinations
|
||||
// even though none can currently
|
||||
pub trait IntoSystem<In, Out, Marker>: Sized {
|
||||
type System: System<In = In, Out = Out>;
|
||||
/// Turns this value into its corresponding [`System`].
|
||||
fn into_system(this: Self) -> Self::System;
|
||||
}
|
||||
|
||||
// Systems implicitly implement IntoSystem
|
||||
impl<In, Out, Sys: System<In = In, Out = Out>> IntoSystem<In, Out, ()> for Sys {
|
||||
type System = Sys;
|
||||
fn into_system(this: Self) -> Sys {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type to mark a [`SystemParam`] as an input.
|
||||
///
|
||||
/// [`System`]s may take an optional input which they require to be passed to them when they
|
||||
/// are being [`run`](System::run). For [`FunctionSystems`](FunctionSystem) the input may be marked
|
||||
/// with this `In` type, but only the first param of a function may be tagged as an input. This also
|
||||
/// means a system can only have one or zero input parameters.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Here is a simple example of a system that takes a [`usize`] returning the square of it.
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut square_system = IntoSystem::into_system(square);
|
||||
///
|
||||
/// let mut world = World::default();
|
||||
/// square_system.initialize(&mut world);
|
||||
/// assert_eq!(square_system.run(12, &mut world), 144);
|
||||
/// }
|
||||
///
|
||||
/// fn square(In(input): In<usize>) -> usize {
|
||||
/// input * input
|
||||
/// }
|
||||
/// ```
|
||||
pub struct In<In>(pub In);
|
||||
|
||||
/// The [`System`] counter part of an ordinary function.
|
||||
///
|
||||
/// You get this by calling [`IntoSystem::into_system`] on a function that only accepts
|
||||
|
@ -479,10 +420,11 @@ where
|
|||
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out {
|
||||
let change_tick = world.increment_change_tick();
|
||||
|
||||
// Safety:
|
||||
// We update the archetype component access correctly based on `Param`'s requirements
|
||||
// in `update_archetype_component_access`.
|
||||
// Our caller upholds the requirements.
|
||||
// SAFETY:
|
||||
// - The caller has invoked `update_archetype_component_access`, which will panic
|
||||
// if the world does not match.
|
||||
// - All world accesses used by `F::Param` have been registered, so the caller
|
||||
// will ensure that there are no data access conflicts.
|
||||
let params = F::Param::get_param(
|
||||
self.param_state.as_mut().expect(Self::PARAM_MESSAGE),
|
||||
&self.system_meta,
|
||||
|
@ -547,7 +489,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// SAFETY: `F`'s param is `ReadOnlySystemParam`, so this system will only read from the world.
|
||||
/// SAFETY: `F`'s param is [`ReadOnlySystemParam`], so this system will only read from the world.
|
||||
unsafe impl<Marker, F> ReadOnlySystem for FunctionSystem<Marker, F>
|
||||
where
|
||||
Marker: 'static,
|
||||
|
|
|
@ -111,7 +111,8 @@ mod query;
|
|||
#[allow(clippy::module_inception)]
|
||||
mod system;
|
||||
mod system_param;
|
||||
mod system_piping;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use combinator::*;
|
||||
pub use commands::*;
|
||||
|
@ -121,7 +122,6 @@ pub use function_system::*;
|
|||
pub use query::*;
|
||||
pub use system::*;
|
||||
pub use system_param::*;
|
||||
pub use system_piping::*;
|
||||
|
||||
use crate::world::World;
|
||||
|
||||
|
@ -190,10 +190,297 @@ where
|
|||
assert_is_system(system);
|
||||
}
|
||||
|
||||
/// Conversion trait to turn something into a [`System`].
|
||||
///
|
||||
/// Use this to get a system from a function. Also note that every system implements this trait as
|
||||
/// well.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn my_system_function(a_usize_local: Local<usize>) {}
|
||||
///
|
||||
/// let system = IntoSystem::into_system(my_system_function);
|
||||
/// ```
|
||||
// This trait has to be generic because we have potentially overlapping impls, in particular
|
||||
// because Rust thinks a type could impl multiple different `FnMut` combinations
|
||||
// even though none can currently
|
||||
pub trait IntoSystem<In, Out, Marker>: Sized {
|
||||
type System: System<In = In, Out = Out>;
|
||||
/// Turns this value into its corresponding [`System`].
|
||||
fn into_system(this: Self) -> Self::System;
|
||||
|
||||
/// Pass the output of this system `A` into a second system `B`, creating a new compound system.
|
||||
///
|
||||
/// The second system must have `In<T>` as its first parameter, where `T`
|
||||
/// is the return type of the first system.
|
||||
fn pipe<B, Final, MarkerB>(self, system: B) -> PipeSystem<Self::System, B::System>
|
||||
where
|
||||
B: IntoSystem<Out, Final, MarkerB>,
|
||||
{
|
||||
let system_a = IntoSystem::into_system(self);
|
||||
let system_b = IntoSystem::into_system(system);
|
||||
let name = format!("Pipe({}, {})", system_a.name(), system_b.name());
|
||||
PipeSystem::new(system_a, system_b, Cow::Owned(name))
|
||||
}
|
||||
}
|
||||
|
||||
// All systems implicitly implement IntoSystem.
|
||||
impl<T: System> IntoSystem<T::In, T::Out, ()> for T {
|
||||
type System = T;
|
||||
fn into_system(this: Self) -> Self {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type to mark a [`SystemParam`] as an input.
|
||||
///
|
||||
/// [`System`]s may take an optional input which they require to be passed to them when they
|
||||
/// are being [`run`](System::run). For [`FunctionSystems`](FunctionSystem) the input may be marked
|
||||
/// with this `In` type, but only the first param of a function may be tagged as an input. This also
|
||||
/// means a system can only have one or zero input parameters.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Here is a simple example of a system that takes a [`usize`] returning the square of it.
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut square_system = IntoSystem::into_system(square);
|
||||
///
|
||||
/// let mut world = World::default();
|
||||
/// square_system.initialize(&mut world);
|
||||
/// assert_eq!(square_system.run(12, &mut world), 144);
|
||||
/// }
|
||||
///
|
||||
/// fn square(In(input): In<usize>) -> usize {
|
||||
/// input * input
|
||||
/// }
|
||||
/// ```
|
||||
pub struct In<In>(pub In);
|
||||
|
||||
/// A collection of common adapters for [piping](crate::system::PipeSystem) the result of a system.
|
||||
pub mod adapter {
|
||||
use crate::system::In;
|
||||
use bevy_utils::tracing;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Converts a regular function into a system adapter.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn return1() -> u64 { 1 }
|
||||
///
|
||||
/// return1
|
||||
/// .pipe(system_adapter::new(u32::try_from))
|
||||
/// .pipe(system_adapter::unwrap)
|
||||
/// .pipe(print);
|
||||
///
|
||||
/// fn print(In(x): In<impl std::fmt::Debug>) {
|
||||
/// println!("{x:?}");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new<T, U>(mut f: impl FnMut(T) -> U) -> impl FnMut(In<T>) -> U {
|
||||
move |In(x)| f(x)
|
||||
}
|
||||
|
||||
/// System adapter that unwraps the `Ok` variant of a [`Result`].
|
||||
/// This is useful for fallible systems that should panic in the case of an error.
|
||||
///
|
||||
/// There is no equivalent adapter for [`Option`]. Instead, it's best to provide
|
||||
/// an error message and convert to a `Result` using `ok_or{_else}`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Panicking on error
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Panic if the load system returns an error.
|
||||
/// load_save_system.pipe(system_adapter::unwrap)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system which may fail irreparably.
|
||||
/// fn load_save_system() -> Result<(), std::io::Error> {
|
||||
/// let save_file = open_file("my_save.json")?;
|
||||
/// dbg!(save_file);
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # fn open_file(name: &str) -> Result<&'static str, std::io::Error>
|
||||
/// # { Ok("hello world") }
|
||||
/// ```
|
||||
pub fn unwrap<T, E: Debug>(In(res): In<Result<T, E>>) -> T {
|
||||
res.unwrap()
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::info!`] macro to print system information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system information.
|
||||
/// data_pipe_system.pipe(system_adapter::info)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a String output.
|
||||
/// fn data_pipe_system() -> String {
|
||||
/// "42".to_string()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn info<T: Debug>(In(data): In<T>) {
|
||||
tracing::info!("{:?}", data);
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::debug!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints debug data from system.
|
||||
/// parse_message_system.pipe(system_adapter::dbg)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<usize, String> output.
|
||||
/// fn parse_message_system() -> Result<usize, std::num::ParseIntError> {
|
||||
/// Ok("42".parse()?)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn dbg<T: Debug>(In(data): In<T>) {
|
||||
tracing::debug!("{:?}", data);
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::warn!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// # let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system warning if system returns an error.
|
||||
/// warning_pipe_system.pipe(system_adapter::warn)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<(), String> output.
|
||||
/// fn warning_pipe_system() -> Result<(), String> {
|
||||
/// Err("Got to rusty?".to_string())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn warn<E: Debug>(In(res): In<Result<(), E>>) {
|
||||
if let Err(warn) = res {
|
||||
tracing::warn!("{:?}", warn);
|
||||
}
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::error!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system error if system fails.
|
||||
/// parse_error_message_system.pipe(system_adapter::error)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<())> output.
|
||||
/// fn parse_error_message_system() -> Result<(), String> {
|
||||
/// Err("Some error".to_owned())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn error<E: Debug>(In(res): In<Result<(), E>>) {
|
||||
if let Err(error) = res {
|
||||
tracing::error!("{:?}", error);
|
||||
}
|
||||
}
|
||||
|
||||
/// System adapter that ignores the output of the previous system in a pipe.
|
||||
/// This is useful for fallible systems that should simply return early in case of an `Err`/`None`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Returning early
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Marker component for an enemy entity.
|
||||
/// #[derive(Component)]
|
||||
/// struct Monster;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// # let mut sched = Schedule::default(); sched
|
||||
/// .add_systems(
|
||||
/// // If the system fails, just move on and try again next frame.
|
||||
/// fallible_system.pipe(system_adapter::ignore)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system which may return early. It's more convenient to use the `?` operator for this.
|
||||
/// fn fallible_system(
|
||||
/// q: Query<Entity, With<Monster>>
|
||||
/// ) -> Option<()> {
|
||||
/// let monster_id = q.iter().next()?;
|
||||
/// println!("Monster entity is {monster_id:?}");
|
||||
/// Some(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn ignore<T>(In(_): In<T>) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::any::TypeId;
|
||||
|
||||
use bevy_utils::default;
|
||||
|
||||
use crate::{
|
||||
self as bevy_ecs,
|
||||
archetype::{ArchetypeComponentId, Archetypes},
|
||||
|
@ -206,8 +493,8 @@ mod tests {
|
|||
removal_detection::RemovedComponents,
|
||||
schedule::{apply_system_buffers, IntoSystemConfigs, Schedule},
|
||||
system::{
|
||||
Commands, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, QueryComponentError,
|
||||
Res, ResMut, Resource, System, SystemState,
|
||||
adapter::new, Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query,
|
||||
QueryComponentError, Res, ResMut, Resource, System, SystemState,
|
||||
},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
|
@ -483,6 +770,13 @@ mod tests {
|
|||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_of_and_without() {
|
||||
fn sys(_: Query<(AnyOf<(&A, &B)>, &mut C)>, _: Query<&mut C, (Without<A>, Without<B>)>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn or_has_no_filter_with() {
|
||||
|
@ -498,6 +792,113 @@ mod tests {
|
|||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_has_filter_with() {
|
||||
fn sys(
|
||||
_: Query<&mut C, Or<(With<A>, With<B>)>>,
|
||||
_: Query<&mut C, (Without<A>, Without<B>)>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_expanded_with_and_without_common() {
|
||||
fn sys(_: Query<&mut D, (With<A>, Or<(With<B>, With<C>)>)>, _: Query<&mut D, Without<A>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_expanded_nested_with_and_without_common() {
|
||||
fn sys(
|
||||
_: Query<&mut E, (Or<((With<B>, With<C>), (With<C>, With<D>))>, With<A>)>,
|
||||
_: Query<&mut E, (Without<B>, Without<D>)>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn or_expanded_nested_with_and_disjoint_without() {
|
||||
fn sys(
|
||||
_: Query<&mut E, (Or<((With<B>, With<C>), (With<C>, With<D>))>, With<A>)>,
|
||||
_: Query<&mut E, Without<D>>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn or_expanded_nested_or_with_and_disjoint_without() {
|
||||
fn sys(
|
||||
_: Query<&mut D, Or<(Or<(With<A>, With<B>)>, Or<(With<A>, With<C>)>)>>,
|
||||
_: Query<&mut D, Without<A>>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_expanded_nested_with_and_common_nested_without() {
|
||||
fn sys(
|
||||
_: Query<&mut D, Or<((With<A>, With<B>), (With<B>, With<C>))>>,
|
||||
_: Query<&mut D, Or<(Without<D>, Without<B>)>>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_with_without_and_compatible_with_without() {
|
||||
fn sys(
|
||||
_: Query<&mut C, Or<(With<A>, Without<B>)>>,
|
||||
_: Query<&mut C, (With<B>, Without<A>)>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn with_and_disjoint_or_empty_without() {
|
||||
fn sys(_: Query<&mut B, With<A>>, _: Query<&mut B, Or<((), Without<A>)>>) {}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn or_expanded_with_and_disjoint_nested_without() {
|
||||
fn sys(
|
||||
_: Query<&mut D, Or<(With<A>, With<B>)>>,
|
||||
_: Query<&mut D, Or<(Without<A>, Without<B>)>>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "error[B0001]"]
|
||||
fn or_expanded_nested_with_and_disjoint_nested_without() {
|
||||
fn sys(
|
||||
_: Query<&mut D, Or<((With<A>, With<B>), (With<B>, With<C>))>>,
|
||||
_: Query<&mut D, Or<(Without<A>, Without<B>)>>,
|
||||
) {
|
||||
}
|
||||
let mut world = World::default();
|
||||
run_system(&mut world, sys);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_doesnt_remove_unrelated_filter_with() {
|
||||
fn sys(_: Query<&mut B, (Or<(With<A>, With<B>)>, With<A>)>, _: Query<&mut B, Without<A>>) {}
|
||||
|
@ -1331,4 +1732,105 @@ mod tests {
|
|||
let mut world = World::new();
|
||||
run_system(&mut world, || panic!("this system panics"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_systems() {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{prelude::*, system::assert_is_system};
|
||||
|
||||
/// Mocks a system that returns a value of type `T`.
|
||||
fn returning<T>() -> T {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Mocks an exclusive system that takes an input and returns an output.
|
||||
fn exclusive_in_out<A, B>(_: In<A>, _: &mut World) -> B {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn not(In(val): In<bool>) -> bool {
|
||||
!val
|
||||
}
|
||||
|
||||
assert_is_system(returning::<Result<u32, std::io::Error>>.pipe(unwrap));
|
||||
assert_is_system(returning::<Option<()>>.pipe(ignore));
|
||||
assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap));
|
||||
assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error));
|
||||
assert_is_system(returning::<bool>.pipe(exclusive_in_out::<bool, ()>));
|
||||
|
||||
returning::<()>.run_if(returning::<bool>.pipe(not));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_change_detection() {
|
||||
#[derive(Resource, Default)]
|
||||
struct Flag;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Info {
|
||||
// If true, the respective system will mutate `Flag`.
|
||||
do_first: bool,
|
||||
do_second: bool,
|
||||
|
||||
// Will be set to true if the respective system saw that `Flag` changed.
|
||||
first_flag: bool,
|
||||
second_flag: bool,
|
||||
}
|
||||
|
||||
fn first(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
|
||||
if flag.is_changed() {
|
||||
info.first_flag = true;
|
||||
}
|
||||
if info.do_first {
|
||||
*flag = Flag;
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
fn second(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
|
||||
if flag.is_changed() {
|
||||
info.second_flag = true;
|
||||
}
|
||||
if info.do_second {
|
||||
*flag = Flag;
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Flag>();
|
||||
let mut sys = first.pipe(second);
|
||||
sys.initialize(&mut world);
|
||||
|
||||
sys.run(default(), &mut world);
|
||||
|
||||
// The second system should observe a change made in the first system.
|
||||
let info = sys.run(
|
||||
Info {
|
||||
do_first: true,
|
||||
..default()
|
||||
},
|
||||
&mut world,
|
||||
);
|
||||
assert!(!info.first_flag);
|
||||
assert!(info.second_flag);
|
||||
|
||||
// When a change is made in the second system, the first system
|
||||
// should observe it the next time they are run.
|
||||
let info1 = sys.run(
|
||||
Info {
|
||||
do_second: true,
|
||||
..default()
|
||||
},
|
||||
&mut world,
|
||||
);
|
||||
let info2 = sys.run(default(), &mut world);
|
||||
assert!(!info1.first_flag);
|
||||
assert!(!info1.second_flag);
|
||||
assert!(info2.first_flag);
|
||||
assert!(!info2.second_flag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,11 +49,17 @@ pub trait System: Send + Sync + 'static {
|
|||
/// 1. This system is the only system running on the given world across all threads.
|
||||
/// 2. This system only runs in parallel with other systems that do not conflict with the
|
||||
/// [`System::archetype_component_access()`].
|
||||
///
|
||||
/// Additionally, the method [`Self::update_archetype_component_access`] must be called at some
|
||||
/// point before this one, with the same exact [`World`]. If `update_archetype_component_access`
|
||||
/// panics (or otherwise does not return for any reason), this method must not be called.
|
||||
unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out;
|
||||
/// Runs the system with the given input in the world.
|
||||
fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out {
|
||||
self.update_archetype_component_access(world);
|
||||
// SAFETY: world and resources are exclusively borrowed
|
||||
// SAFETY:
|
||||
// - World and resources are exclusively borrowed, which ensures no data access conflicts.
|
||||
// - `update_archetype_component_access` has been called.
|
||||
unsafe { self.run_unsafe(input, world) }
|
||||
}
|
||||
fn apply_buffers(&mut self, world: &mut World);
|
||||
|
|
|
@ -1,422 +0,0 @@
|
|||
use crate::system::{IntoSystem, System};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{CombinatorSystem, Combine};
|
||||
|
||||
/// A [`System`] created by piping the output of the first system into the input of the second.
|
||||
///
|
||||
/// This can be repeated indefinitely, but system pipes cannot branch: the output is consumed by the receiving system.
|
||||
///
|
||||
/// Given two systems `A` and `B`, A may be piped into `B` as `A.pipe(B)` if the output type of `A` is
|
||||
/// equal to the input type of `B`.
|
||||
///
|
||||
/// Note that for [`FunctionSystem`](crate::system::FunctionSystem)s the output is the return value
|
||||
/// of the function and the input is the first [`SystemParam`](crate::system::SystemParam) if it is
|
||||
/// tagged with [`In`](crate::system::In) or `()` if the function has no designated input parameter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::ParseIntError;
|
||||
///
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut world = World::default();
|
||||
/// world.insert_resource(Message("42".to_string()));
|
||||
///
|
||||
/// // pipe the `parse_message_system`'s output into the `filter_system`s input
|
||||
/// let mut piped_system = parse_message_system.pipe(filter_system);
|
||||
/// piped_system.initialize(&mut world);
|
||||
/// assert_eq!(piped_system.run((), &mut world), Some(42));
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Resource)]
|
||||
/// struct Message(String);
|
||||
///
|
||||
/// fn parse_message_system(message: Res<Message>) -> Result<usize, ParseIntError> {
|
||||
/// message.0.parse::<usize>()
|
||||
/// }
|
||||
///
|
||||
/// fn filter_system(In(result): In<Result<usize, ParseIntError>>) -> Option<usize> {
|
||||
/// result.ok().filter(|&n| n < 100)
|
||||
/// }
|
||||
/// ```
|
||||
pub type PipeSystem<SystemA, SystemB> = CombinatorSystem<Pipe, SystemA, SystemB>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct Pipe;
|
||||
|
||||
impl<A, B> Combine<A, B> for Pipe
|
||||
where
|
||||
A: System,
|
||||
B: System<In = A::Out>,
|
||||
{
|
||||
type In = A::In;
|
||||
type Out = B::Out;
|
||||
|
||||
fn combine(
|
||||
input: Self::In,
|
||||
a: impl FnOnce(<A as System>::In) -> <A as System>::Out,
|
||||
b: impl FnOnce(<B as System>::In) -> <B as System>::Out,
|
||||
) -> Self::Out {
|
||||
let value = a(input);
|
||||
b(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait providing the [`IntoPipeSystem::pipe`] method to pass input from one system into the next.
|
||||
///
|
||||
/// The first system must have return type `T`
|
||||
/// and the second system must have [`In<T>`](crate::system::In) as its first system parameter.
|
||||
///
|
||||
/// This trait is blanket implemented for all system pairs that fulfill the type requirements.
|
||||
///
|
||||
/// See [`PipeSystem`].
|
||||
pub trait IntoPipeSystem<ParamA, Payload, SystemB, ParamB, Out>:
|
||||
IntoSystem<(), Payload, ParamA> + Sized
|
||||
where
|
||||
SystemB: IntoSystem<Payload, Out, ParamB>,
|
||||
{
|
||||
/// Pass the output of this system `A` into a second system `B`, creating a new compound system.
|
||||
fn pipe(self, system: SystemB) -> PipeSystem<Self::System, SystemB::System>;
|
||||
}
|
||||
|
||||
impl<SystemA, ParamA, Payload, SystemB, ParamB, Out>
|
||||
IntoPipeSystem<ParamA, Payload, SystemB, ParamB, Out> for SystemA
|
||||
where
|
||||
SystemA: IntoSystem<(), Payload, ParamA>,
|
||||
SystemB: IntoSystem<Payload, Out, ParamB>,
|
||||
{
|
||||
fn pipe(self, system: SystemB) -> PipeSystem<SystemA::System, SystemB::System> {
|
||||
let system_a = IntoSystem::into_system(self);
|
||||
let system_b = IntoSystem::into_system(system);
|
||||
let name = format!("Pipe({}, {})", system_a.name(), system_b.name());
|
||||
PipeSystem::new(system_a, system_b, Cow::Owned(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of common adapters for [piping](super::PipeSystem) the result of a system.
|
||||
pub mod adapter {
|
||||
use crate::system::In;
|
||||
use bevy_utils::tracing;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Converts a regular function into a system adapter.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// fn return1() -> u64 { 1 }
|
||||
///
|
||||
/// return1
|
||||
/// .pipe(system_adapter::new(u32::try_from))
|
||||
/// .pipe(system_adapter::unwrap)
|
||||
/// .pipe(print);
|
||||
///
|
||||
/// fn print(In(x): In<impl std::fmt::Debug>) {
|
||||
/// println!("{x:?}");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new<T, U>(mut f: impl FnMut(T) -> U) -> impl FnMut(In<T>) -> U {
|
||||
move |In(x)| f(x)
|
||||
}
|
||||
|
||||
/// System adapter that unwraps the `Ok` variant of a [`Result`].
|
||||
/// This is useful for fallible systems that should panic in the case of an error.
|
||||
///
|
||||
/// There is no equivalent adapter for [`Option`]. Instead, it's best to provide
|
||||
/// an error message and convert to a `Result` using `ok_or{_else}`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Panicking on error
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Panic if the load system returns an error.
|
||||
/// load_save_system.pipe(system_adapter::unwrap)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system which may fail irreparably.
|
||||
/// fn load_save_system() -> Result<(), std::io::Error> {
|
||||
/// let save_file = open_file("my_save.json")?;
|
||||
/// dbg!(save_file);
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # fn open_file(name: &str) -> Result<&'static str, std::io::Error>
|
||||
/// # { Ok("hello world") }
|
||||
/// ```
|
||||
pub fn unwrap<T, E: Debug>(In(res): In<Result<T, E>>) -> T {
|
||||
res.unwrap()
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::info!`] macro to print system information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system information.
|
||||
/// data_pipe_system.pipe(system_adapter::info)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a String output.
|
||||
/// fn data_pipe_system() -> String {
|
||||
/// "42".to_string()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn info<T: Debug>(In(data): In<T>) {
|
||||
tracing::info!("{:?}", data);
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::debug!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints debug data from system.
|
||||
/// parse_message_system.pipe(system_adapter::dbg)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<usize, String> output.
|
||||
/// fn parse_message_system() -> Result<usize, std::num::ParseIntError> {
|
||||
/// Ok("42".parse()?)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn dbg<T: Debug>(In(data): In<T>) {
|
||||
tracing::debug!("{:?}", data);
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::warn!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// # let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system warning if system returns an error.
|
||||
/// warning_pipe_system.pipe(system_adapter::warn)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<(), String> output.
|
||||
/// fn warning_pipe_system() -> Result<(), String> {
|
||||
/// Err("Got to rusty?".to_string())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn warn<E: Debug>(In(res): In<Result<(), E>>) {
|
||||
if let Err(warn) = res {
|
||||
tracing::warn!("{:?}", warn);
|
||||
}
|
||||
}
|
||||
|
||||
/// System adapter that utilizes the [`bevy_utils::tracing::error!`] macro to print the output of a system.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
/// // Building a new schedule/app...
|
||||
/// let mut sched = Schedule::default();
|
||||
/// sched.add_systems(
|
||||
/// // Prints system error if system fails.
|
||||
/// parse_error_message_system.pipe(system_adapter::error)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system that returns a Result<())> output.
|
||||
/// fn parse_error_message_system() -> Result<(), String> {
|
||||
/// Err("Some error".to_owned())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn error<E: Debug>(In(res): In<Result<(), E>>) {
|
||||
if let Err(error) = res {
|
||||
tracing::error!("{:?}", error);
|
||||
}
|
||||
}
|
||||
|
||||
/// System adapter that ignores the output of the previous system in a pipe.
|
||||
/// This is useful for fallible systems that should simply return early in case of an `Err`/`None`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Returning early
|
||||
///
|
||||
/// ```
|
||||
/// use bevy_ecs::prelude::*;
|
||||
///
|
||||
/// // Marker component for an enemy entity.
|
||||
/// #[derive(Component)]
|
||||
/// struct Monster;
|
||||
///
|
||||
/// // Building a new schedule/app...
|
||||
/// # let mut sched = Schedule::default(); sched
|
||||
/// .add_systems(
|
||||
/// // If the system fails, just move on and try again next frame.
|
||||
/// fallible_system.pipe(system_adapter::ignore)
|
||||
/// )
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// # let mut world = World::new();
|
||||
/// # sched.run(&mut world);
|
||||
///
|
||||
/// // A system which may return early. It's more convenient to use the `?` operator for this.
|
||||
/// fn fallible_system(
|
||||
/// q: Query<Entity, With<Monster>>
|
||||
/// ) -> Option<()> {
|
||||
/// let monster_id = q.iter().next()?;
|
||||
/// println!("Monster entity is {monster_id:?}");
|
||||
/// Some(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn ignore<T>(In(_): In<T>) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_utils::default;
|
||||
|
||||
use super::adapter::*;
|
||||
use crate::{self as bevy_ecs, prelude::*, system::PipeSystem};
|
||||
|
||||
#[test]
|
||||
fn assert_systems() {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{prelude::*, system::assert_is_system};
|
||||
|
||||
/// Mocks a system that returns a value of type `T`.
|
||||
fn returning<T>() -> T {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Mocks an exclusive system that takes an input and returns an output.
|
||||
fn exclusive_in_out<A, B>(_: In<A>, _: &mut World) -> B {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn not(In(val): In<bool>) -> bool {
|
||||
!val
|
||||
}
|
||||
|
||||
assert_is_system(returning::<Result<u32, std::io::Error>>.pipe(unwrap));
|
||||
assert_is_system(returning::<Option<()>>.pipe(ignore));
|
||||
assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap));
|
||||
assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error));
|
||||
assert_is_system(returning::<bool>.pipe(exclusive_in_out::<bool, ()>));
|
||||
|
||||
returning::<()>.run_if(returning::<bool>.pipe(not));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_change_detection() {
|
||||
#[derive(Resource, Default)]
|
||||
struct Flag;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Info {
|
||||
// If true, the respective system will mutate `Flag`.
|
||||
do_first: bool,
|
||||
do_second: bool,
|
||||
|
||||
// Will be set to true if the respective system saw that `Flag` changed.
|
||||
first_flag: bool,
|
||||
second_flag: bool,
|
||||
}
|
||||
|
||||
fn first(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
|
||||
if flag.is_changed() {
|
||||
info.first_flag = true;
|
||||
}
|
||||
if info.do_first {
|
||||
*flag = Flag;
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
fn second(In(mut info): In<Info>, mut flag: ResMut<Flag>) -> Info {
|
||||
if flag.is_changed() {
|
||||
info.second_flag = true;
|
||||
}
|
||||
if info.do_second {
|
||||
*flag = Flag;
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Flag>();
|
||||
let mut sys = PipeSystem::new(
|
||||
IntoSystem::into_system(first),
|
||||
IntoSystem::into_system(second),
|
||||
"".into(),
|
||||
);
|
||||
sys.initialize(&mut world);
|
||||
|
||||
sys.run(default(), &mut world);
|
||||
|
||||
// The second system should observe a change made in the first system.
|
||||
let info = sys.run(
|
||||
Info {
|
||||
do_first: true,
|
||||
..default()
|
||||
},
|
||||
&mut world,
|
||||
);
|
||||
assert!(!info.first_flag);
|
||||
assert!(info.second_flag);
|
||||
|
||||
// When a change is made in the second system, the first system
|
||||
// should observe it the next time they are run.
|
||||
let info1 = sys.run(
|
||||
Info {
|
||||
do_second: true,
|
||||
..default()
|
||||
},
|
||||
&mut world,
|
||||
);
|
||||
let info2 = sys.run(default(), &mut world);
|
||||
assert!(!info1.first_flag);
|
||||
assert!(!info1.second_flag);
|
||||
assert!(info2.first_flag);
|
||||
assert!(!info2.second_flag);
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ pub struct World {
|
|||
pub(crate) storages: Storages,
|
||||
pub(crate) bundles: Bundles,
|
||||
pub(crate) removed_components: RemovedComponentEvents,
|
||||
/// Access cache used by [WorldCell]. Is only accessed in the `Drop` impl of `WorldCell`.
|
||||
/// Access cache used by [`WorldCell`]. Is only accessed in the `Drop` impl of `WorldCell`.
|
||||
pub(crate) archetype_component_access: ArchetypeComponentAccess,
|
||||
pub(crate) change_tick: AtomicU32,
|
||||
pub(crate) last_change_tick: Tick,
|
||||
|
@ -1734,30 +1734,10 @@ impl World {
|
|||
/// For other use cases, see the example on [`World::schedule_scope`].
|
||||
pub fn try_schedule_scope<R>(
|
||||
&mut self,
|
||||
label: impl ScheduleLabel,
|
||||
f: impl FnOnce(&mut World, &mut Schedule) -> R,
|
||||
) -> Result<R, TryRunScheduleError> {
|
||||
self.try_schedule_scope_ref(&label, f)
|
||||
}
|
||||
|
||||
/// Temporarily removes the schedule associated with `label` from the world,
|
||||
/// runs user code, and finally re-adds the schedule.
|
||||
/// This returns a [`TryRunScheduleError`] if there is no schedule
|
||||
/// associated with `label`.
|
||||
///
|
||||
/// Unlike the `try_run_schedule` method, this method takes the label by reference, which can save a clone.
|
||||
///
|
||||
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
|
||||
/// and system state is cached.
|
||||
///
|
||||
/// For simple cases where you just need to call the schedule once,
|
||||
/// consider using [`World::try_run_schedule_ref`] instead.
|
||||
/// For other use cases, see the example on [`World::schedule_scope`].
|
||||
pub fn try_schedule_scope_ref<R>(
|
||||
&mut self,
|
||||
label: &dyn ScheduleLabel,
|
||||
label: impl AsRef<dyn ScheduleLabel>,
|
||||
f: impl FnOnce(&mut World, &mut Schedule) -> R,
|
||||
) -> Result<R, TryRunScheduleError> {
|
||||
let label = label.as_ref();
|
||||
let Some((extracted_label, mut schedule))
|
||||
= self.get_resource_mut::<Schedules>().and_then(|mut s| s.remove_entry(label))
|
||||
else {
|
||||
|
@ -1818,33 +1798,10 @@ impl World {
|
|||
/// If the requested schedule does not exist.
|
||||
pub fn schedule_scope<R>(
|
||||
&mut self,
|
||||
label: impl ScheduleLabel,
|
||||
label: impl AsRef<dyn ScheduleLabel>,
|
||||
f: impl FnOnce(&mut World, &mut Schedule) -> R,
|
||||
) -> R {
|
||||
self.schedule_scope_ref(&label, f)
|
||||
}
|
||||
|
||||
/// Temporarily removes the schedule associated with `label` from the world,
|
||||
/// runs user code, and finally re-adds the schedule.
|
||||
///
|
||||
/// Unlike the `run_schedule` method, this method takes the label by reference, which can save a clone.
|
||||
///
|
||||
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
|
||||
/// and system state is cached.
|
||||
///
|
||||
/// For simple cases where you just need to call the schedule,
|
||||
/// consider using [`World::run_schedule_ref`] instead.
|
||||
/// For other use cases, see the example on [`World::schedule_scope`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the requested schedule does not exist.
|
||||
pub fn schedule_scope_ref<R>(
|
||||
&mut self,
|
||||
label: &dyn ScheduleLabel,
|
||||
f: impl FnOnce(&mut World, &mut Schedule) -> R,
|
||||
) -> R {
|
||||
self.try_schedule_scope_ref(label, f)
|
||||
self.try_schedule_scope(label, f)
|
||||
.unwrap_or_else(|e| panic!("{e}"))
|
||||
}
|
||||
|
||||
|
@ -1857,25 +1814,9 @@ impl World {
|
|||
/// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead.
|
||||
pub fn try_run_schedule(
|
||||
&mut self,
|
||||
label: impl ScheduleLabel,
|
||||
label: impl AsRef<dyn ScheduleLabel>,
|
||||
) -> Result<(), TryRunScheduleError> {
|
||||
self.try_run_schedule_ref(&label)
|
||||
}
|
||||
|
||||
/// Attempts to run the [`Schedule`] associated with the `label` a single time,
|
||||
/// and returns a [`TryRunScheduleError`] if the schedule does not exist.
|
||||
///
|
||||
/// Unlike the `try_run_schedule` method, this method takes the label by reference, which can save a clone.
|
||||
///
|
||||
/// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label,
|
||||
/// and system state is cached.
|
||||
///
|
||||
/// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead.
|
||||
pub fn try_run_schedule_ref(
|
||||
&mut self,
|
||||
label: &dyn ScheduleLabel,
|
||||
) -> Result<(), TryRunScheduleError> {
|
||||
self.try_schedule_scope_ref(label, |world, sched| sched.run(world))
|
||||
self.try_schedule_scope(label, |world, sched| sched.run(world))
|
||||
}
|
||||
|
||||
/// Runs the [`Schedule`] associated with the `label` a single time.
|
||||
|
@ -1888,8 +1829,8 @@ impl World {
|
|||
/// # Panics
|
||||
///
|
||||
/// If the requested schedule does not exist.
|
||||
pub fn run_schedule(&mut self, label: impl ScheduleLabel) {
|
||||
self.run_schedule_ref(&label);
|
||||
pub fn run_schedule(&mut self, label: impl AsRef<dyn ScheduleLabel>) {
|
||||
self.schedule_scope(label, |world, sched| sched.run(world));
|
||||
}
|
||||
|
||||
/// Runs the [`Schedule`] associated with the `label` a single time.
|
||||
|
@ -1904,8 +1845,9 @@ impl World {
|
|||
/// # Panics
|
||||
///
|
||||
/// If the requested schedule does not exist.
|
||||
#[deprecated = "Use `World::run_schedule` instead."]
|
||||
pub fn run_schedule_ref(&mut self, label: &dyn ScheduleLabel) {
|
||||
self.schedule_scope_ref(label, |world, sched| sched.run(world));
|
||||
self.schedule_scope(label, |world, sched| sched.run(world));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,9 @@ error[E0499]: cannot borrow `e_mut` as mutable more than once at a time
|
|||
error[E0505]: cannot move out of `e_mut` because it is borrowed
|
||||
--> tests/ui/entity_ref_mut_lifetime_safety.rs:33:9
|
||||
|
|
||||
13 | let mut e_mut = world.entity_mut(e);
|
||||
| --------- binding `e_mut` declared here
|
||||
...
|
||||
32 | let gotten: &A = e_mut.get::<A>().unwrap();
|
||||
| ---------------- borrow of `e_mut` occurs here
|
||||
33 | e_mut.despawn();
|
||||
|
|
|
@ -30,7 +30,7 @@ note: required by a bound in `assert_readonly`
|
|||
--> tests/ui/system_param_derive_readonly.rs:23:8
|
||||
|
|
||||
21 | fn assert_readonly<P>()
|
||||
| --------------- required by a bound in this
|
||||
| --------------- required by a bound in this function
|
||||
22 | where
|
||||
23 | P: ReadOnlySystemParam,
|
||||
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_readonly`
|
||||
|
|
|
@ -13,7 +13,10 @@ keywords = ["bevy"]
|
|||
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
|
||||
bevy_input = { path = "../bevy_input", version = "0.11.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.11.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
||||
bevy_time = { path = "../bevy_time", version = "0.11.0-dev" }
|
||||
|
||||
# other
|
||||
gilrs = "0.10.1"
|
||||
thiserror = "1.0"
|
|
@ -2,17 +2,23 @@
|
|||
|
||||
mod converter;
|
||||
mod gilrs_system;
|
||||
mod rumble;
|
||||
|
||||
use bevy_app::{App, Plugin, PreStartup, PreUpdate};
|
||||
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_input::InputSystem;
|
||||
use bevy_utils::tracing::error;
|
||||
use gilrs::GilrsBuilder;
|
||||
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
|
||||
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GilrsPlugin;
|
||||
|
||||
/// Updates the running gamepad rumble effects.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
|
||||
pub struct RumbleSystem;
|
||||
|
||||
impl Plugin for GilrsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
match GilrsBuilder::new()
|
||||
|
@ -22,8 +28,10 @@ impl Plugin for GilrsPlugin {
|
|||
{
|
||||
Ok(gilrs) => {
|
||||
app.insert_non_send_resource(gilrs)
|
||||
.init_non_send_resource::<RunningRumbleEffects>()
|
||||
.add_systems(PreStartup, gilrs_event_startup_system)
|
||||
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem));
|
||||
.add_systems(PreUpdate, gilrs_event_system.before(InputSystem))
|
||||
.add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystem));
|
||||
}
|
||||
Err(err) => error!("Failed to start Gilrs. {}", err),
|
||||
}
|
||||
|
|
177
crates/bevy_gilrs/src/rumble.rs
Normal file
177
crates/bevy_gilrs/src/rumble.rs
Normal file
|
@ -0,0 +1,177 @@
|
|||
//! Handle user specified rumble request events.
|
||||
use bevy_ecs::{
|
||||
prelude::{EventReader, Res},
|
||||
system::NonSendMut,
|
||||
};
|
||||
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
|
||||
use bevy_log::{debug, warn};
|
||||
use bevy_time::Time;
|
||||
use bevy_utils::{Duration, HashMap};
|
||||
use gilrs::{
|
||||
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
|
||||
GamepadId, Gilrs,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::converter::convert_gamepad_id;
|
||||
|
||||
/// A rumble effect that is currently in effect.
|
||||
struct RunningRumble {
|
||||
/// Duration from app startup when this effect will be finished
|
||||
deadline: Duration,
|
||||
/// A ref-counted handle to the specific force-feedback effect
|
||||
///
|
||||
/// Dropping it will cause the effect to stop
|
||||
#[allow(dead_code)]
|
||||
effect: ff::Effect,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum RumbleError {
|
||||
#[error("gamepad not found")]
|
||||
GamepadNotFound,
|
||||
#[error("gilrs error while rumbling gamepad: {0}")]
|
||||
GilrsError(#[from] ff::Error),
|
||||
}
|
||||
|
||||
/// Contains the gilrs rumble effects that are currently running for each gamepad
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RunningRumbleEffects {
|
||||
/// If multiple rumbles are running at the same time, their resulting rumble
|
||||
/// will be the saturated sum of their strengths up until [`u16::MAX`]
|
||||
rumbles: HashMap<GamepadId, Vec<RunningRumble>>,
|
||||
}
|
||||
|
||||
/// gilrs uses magnitudes from 0 to [`u16::MAX`], while ours go from `0.0` to `1.0` ([`f32`])
|
||||
fn to_gilrs_magnitude(ratio: f32) -> u16 {
|
||||
(ratio * u16::MAX as f32) as u16
|
||||
}
|
||||
|
||||
fn get_base_effects(
|
||||
GamepadRumbleIntensity {
|
||||
weak_motor,
|
||||
strong_motor,
|
||||
}: GamepadRumbleIntensity,
|
||||
duration: Duration,
|
||||
) -> Vec<ff::BaseEffect> {
|
||||
let mut effects = Vec::new();
|
||||
if strong_motor > 0. {
|
||||
effects.push(BaseEffect {
|
||||
kind: BaseEffectType::Strong {
|
||||
magnitude: to_gilrs_magnitude(strong_motor),
|
||||
},
|
||||
scheduling: Replay {
|
||||
play_for: duration.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if weak_motor > 0. {
|
||||
effects.push(BaseEffect {
|
||||
kind: BaseEffectType::Strong {
|
||||
magnitude: to_gilrs_magnitude(weak_motor),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
effects
|
||||
}
|
||||
|
||||
fn handle_rumble_request(
|
||||
running_rumbles: &mut RunningRumbleEffects,
|
||||
gilrs: &mut Gilrs,
|
||||
rumble: GamepadRumbleRequest,
|
||||
current_time: Duration,
|
||||
) -> Result<(), RumbleError> {
|
||||
let gamepad = rumble.gamepad();
|
||||
|
||||
let (gamepad_id, _) = gilrs
|
||||
.gamepads()
|
||||
.find(|(pad_id, _)| convert_gamepad_id(*pad_id) == gamepad)
|
||||
.ok_or(RumbleError::GamepadNotFound)?;
|
||||
|
||||
match rumble {
|
||||
GamepadRumbleRequest::Stop { .. } => {
|
||||
// `ff::Effect` uses RAII, dropping = deactivating
|
||||
running_rumbles.rumbles.remove(&gamepad_id);
|
||||
}
|
||||
GamepadRumbleRequest::Add {
|
||||
duration,
|
||||
intensity,
|
||||
..
|
||||
} => {
|
||||
let mut effect_builder = ff::EffectBuilder::new();
|
||||
|
||||
for effect in get_base_effects(intensity, duration) {
|
||||
effect_builder.add_effect(effect);
|
||||
effect_builder.repeat(Repeat::For(duration.into()));
|
||||
}
|
||||
|
||||
let effect = effect_builder.gamepads(&[gamepad_id]).finish(gilrs)?;
|
||||
effect.play()?;
|
||||
|
||||
let gamepad_rumbles = running_rumbles.rumbles.entry(gamepad_id).or_default();
|
||||
let deadline = current_time + duration;
|
||||
gamepad_rumbles.push(RunningRumble { deadline, effect });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn play_gilrs_rumble(
|
||||
time: Res<Time>,
|
||||
mut gilrs: NonSendMut<Gilrs>,
|
||||
mut requests: EventReader<GamepadRumbleRequest>,
|
||||
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
|
||||
) {
|
||||
let current_time = time.raw_elapsed();
|
||||
// Remove outdated rumble effects.
|
||||
for rumbles in running_rumbles.rumbles.values_mut() {
|
||||
// `ff::Effect` uses RAII, dropping = deactivating
|
||||
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
|
||||
}
|
||||
running_rumbles
|
||||
.rumbles
|
||||
.retain(|_gamepad, rumbles| !rumbles.is_empty());
|
||||
|
||||
// Add new effects.
|
||||
for rumble in requests.iter().cloned() {
|
||||
let gamepad = rumble.gamepad();
|
||||
match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) {
|
||||
Ok(()) => {}
|
||||
Err(RumbleError::GilrsError(err)) => {
|
||||
if let ff::Error::FfNotSupported(_) = err {
|
||||
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
|
||||
} else {
|
||||
warn!(
|
||||
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(RumbleError::GamepadNotFound) => {
|
||||
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::to_gilrs_magnitude;
|
||||
|
||||
#[test]
|
||||
fn magnitude_conversion() {
|
||||
assert_eq!(to_gilrs_magnitude(1.0), u16::MAX);
|
||||
assert_eq!(to_gilrs_magnitude(0.0), 0);
|
||||
|
||||
// bevy magnitudes of 2.0 don't really make sense, but just make sure
|
||||
// they convert to something sensible in gilrs anyway.
|
||||
assert_eq!(to_gilrs_magnitude(2.0), u16::MAX);
|
||||
|
||||
// negative bevy magnitudes don't really make sense, but just make sure
|
||||
// they convert to something sensible in gilrs anyway.
|
||||
assert_eq!(to_gilrs_magnitude(-1.0), 0);
|
||||
assert_eq!(to_gilrs_magnitude(-0.1), 0);
|
||||
}
|
||||
}
|
|
@ -21,3 +21,4 @@ bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
|
|||
bevy_core = { path = "../bevy_core", version = "0.11.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.11.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.11.0-dev" }
|
||||
|
|
|
@ -7,7 +7,8 @@ use bevy_ecs::{
|
|||
world::World,
|
||||
};
|
||||
use bevy_math::{Mat2, Quat, Vec2, Vec3};
|
||||
use bevy_render::prelude::Color;
|
||||
use bevy_render::color::Color;
|
||||
use bevy_transform::TransformPoint;
|
||||
|
||||
type PositionItem = [f32; 3];
|
||||
type ColorItem = [f32; 4];
|
||||
|
@ -280,27 +281,31 @@ impl<'s> Gizmos<'s> {
|
|||
/// ```
|
||||
/// # use bevy_gizmos::prelude::*;
|
||||
/// # use bevy_render::prelude::*;
|
||||
/// # use bevy_math::prelude::*;
|
||||
/// # use bevy_transform::prelude::*;
|
||||
/// fn system(mut gizmos: Gizmos) {
|
||||
/// gizmos.cuboid(Vec3::ZERO, Quat::IDENTITY, Vec3::ONE, Color::GREEN);
|
||||
/// gizmos.cuboid(Transform::IDENTITY, Color::GREEN);
|
||||
/// }
|
||||
/// # bevy_ecs::system::assert_is_system(system);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn cuboid(&mut self, position: Vec3, rotation: Quat, size: Vec3, color: Color) {
|
||||
let rect = rect_inner(size.truncate());
|
||||
pub fn cuboid(&mut self, transform: impl TransformPoint, color: Color) {
|
||||
let rect = rect_inner(Vec2::ONE);
|
||||
// Front
|
||||
let [tlf, trf, brf, blf] = rect.map(|vec2| position + rotation * vec2.extend(size.z / 2.));
|
||||
let [tlf, trf, brf, blf] = rect.map(|vec2| transform.transform_point(vec2.extend(0.5)));
|
||||
// Back
|
||||
let [tlb, trb, brb, blb] = rect.map(|vec2| position + rotation * vec2.extend(-size.z / 2.));
|
||||
let [tlb, trb, brb, blb] = rect.map(|vec2| transform.transform_point(vec2.extend(-0.5)));
|
||||
|
||||
let positions = [
|
||||
tlf, trf, trf, brf, brf, blf, blf, tlf, // Front
|
||||
tlb, trb, trb, brb, brb, blb, blb, tlb, // Back
|
||||
tlf, tlb, trf, trb, brf, brb, blf, blb, // Front to back
|
||||
let strip_positions = [
|
||||
tlf, trf, brf, blf, tlf, // Front
|
||||
tlb, trb, brb, blb, tlb, // Back
|
||||
];
|
||||
self.extend_list_positions(positions);
|
||||
self.add_list_color(color, 24);
|
||||
self.linestrip(strip_positions, color);
|
||||
|
||||
let list_positions = [
|
||||
trf, trb, brf, brb, blf, blb, // Front to back
|
||||
];
|
||||
self.extend_list_positions(list_positions);
|
||||
self.add_list_color(color, 6);
|
||||
}
|
||||
|
||||
/// Draw a line from `start` to `end`.
|
||||
|
@ -459,6 +464,51 @@ impl<'s> Gizmos<'s> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw an arc, which is a part of the circumference of a circle.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `position` sets the center of this circle.
|
||||
/// - `radius` controls the distance from `position` to this arc, and thus its curvature.
|
||||
/// - `direction_angle` sets the angle in radians between `position` and the midpoint of the arc.
|
||||
/// -`arc_angle` sets the length of this arc, in radians.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_gizmos::prelude::*;
|
||||
/// # use bevy_render::prelude::*;
|
||||
/// # use bevy_math::prelude::*;
|
||||
/// # use std::f32::consts::PI;
|
||||
/// fn system(mut gizmos: Gizmos) {
|
||||
/// gizmos.arc_2d(Vec2::ZERO, 0., PI / 4., 1., Color::GREEN);
|
||||
///
|
||||
/// // Arcs have 32 line-segments by default.
|
||||
/// // You may want to increase this for larger arcs.
|
||||
/// gizmos
|
||||
/// .arc_2d(Vec2::ZERO, 0., PI / 4., 5., Color::RED)
|
||||
/// .segments(64);
|
||||
/// }
|
||||
/// # bevy_ecs::system::assert_is_system(system);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn arc_2d(
|
||||
&mut self,
|
||||
position: Vec2,
|
||||
direction_angle: f32,
|
||||
arc_angle: f32,
|
||||
radius: f32,
|
||||
color: Color,
|
||||
) -> Arc2dBuilder<'_, 's> {
|
||||
Arc2dBuilder {
|
||||
gizmos: self,
|
||||
position,
|
||||
direction_angle,
|
||||
arc_angle,
|
||||
radius,
|
||||
color,
|
||||
segments: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a wireframe rectangle.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -589,6 +639,54 @@ impl Drop for Circle2dBuilder<'_, '_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A builder returned by [`Gizmos::arc_2d`].
|
||||
pub struct Arc2dBuilder<'a, 's> {
|
||||
gizmos: &'a mut Gizmos<'s>,
|
||||
position: Vec2,
|
||||
direction_angle: f32,
|
||||
arc_angle: f32,
|
||||
radius: f32,
|
||||
color: Color,
|
||||
segments: Option<usize>,
|
||||
}
|
||||
|
||||
impl Arc2dBuilder<'_, '_> {
|
||||
/// Set the number of line-segements for this arc.
|
||||
pub fn segments(mut self, segments: usize) -> Self {
|
||||
self.segments = Some(segments);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Arc2dBuilder<'_, '_> {
|
||||
fn drop(&mut self) {
|
||||
let segments = match self.segments {
|
||||
Some(segments) => segments,
|
||||
// Do a linear interpolation between 1 and `DEFAULT_CIRCLE_SEGMENTS`
|
||||
// using the arc angle as scalar.
|
||||
None => ((self.arc_angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize,
|
||||
};
|
||||
|
||||
let positions = arc_inner(self.direction_angle, self.arc_angle, self.radius, segments)
|
||||
.map(|vec2| (vec2 + self.position));
|
||||
self.gizmos.linestrip_2d(positions, self.color);
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_inner(
|
||||
direction_angle: f32,
|
||||
arc_angle: f32,
|
||||
radius: f32,
|
||||
segments: usize,
|
||||
) -> impl Iterator<Item = Vec2> {
|
||||
(0..segments + 1).map(move |i| {
|
||||
let start = direction_angle - arc_angle / 2.;
|
||||
|
||||
let angle = start + (i as f32 * (arc_angle / segments as f32));
|
||||
Vec2::from(angle.sin_cos()) * radius
|
||||
})
|
||||
}
|
||||
|
||||
fn circle_inner(radius: f32, segments: usize) -> impl Iterator<Item = Vec2> {
|
||||
(0..segments + 1).map(move |i| {
|
||||
let angle = i as f32 * TAU / segments as f32;
|
||||
|
|
|
@ -18,22 +18,31 @@
|
|||
|
||||
use std::mem;
|
||||
|
||||
use bevy_app::{Last, Plugin};
|
||||
use bevy_app::{Last, Plugin, Update};
|
||||
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
|
||||
use bevy_ecs::{
|
||||
prelude::{Component, DetectChanges},
|
||||
change_detection::DetectChanges,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
query::Without,
|
||||
reflect::ReflectComponent,
|
||||
schedule::IntoSystemConfigs,
|
||||
system::{Commands, Res, ResMut, Resource},
|
||||
system::{Commands, Query, Res, ResMut, Resource},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_math::Mat4;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_reflect::{
|
||||
std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypeUuid,
|
||||
};
|
||||
use bevy_render::{
|
||||
color::Color,
|
||||
mesh::Mesh,
|
||||
primitives::Aabb,
|
||||
render_phase::AddRenderCommand,
|
||||
render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines},
|
||||
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_transform::components::{GlobalTransform, Transform};
|
||||
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
use bevy_pbr::MeshUniform;
|
||||
|
@ -47,12 +56,12 @@ mod pipeline_2d;
|
|||
#[cfg(feature = "bevy_pbr")]
|
||||
mod pipeline_3d;
|
||||
|
||||
use crate::gizmos::GizmoStorage;
|
||||
use gizmos::{GizmoStorage, Gizmos};
|
||||
|
||||
/// The `bevy_gizmos` prelude.
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{gizmos::Gizmos, GizmoConfig};
|
||||
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
|
||||
}
|
||||
|
||||
const LINE_SHADER_HANDLE: HandleUntyped =
|
||||
|
@ -68,7 +77,14 @@ impl Plugin for GizmoPlugin {
|
|||
app.init_resource::<MeshHandles>()
|
||||
.init_resource::<GizmoConfig>()
|
||||
.init_resource::<GizmoStorage>()
|
||||
.add_systems(Last, update_gizmo_meshes);
|
||||
.add_systems(Last, update_gizmo_meshes)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
draw_aabbs,
|
||||
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
|
||||
),
|
||||
);
|
||||
|
||||
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
|
||||
|
||||
|
@ -101,7 +117,7 @@ impl Plugin for GizmoPlugin {
|
|||
}
|
||||
|
||||
/// A [`Resource`] that stores configuration for gizmos.
|
||||
#[derive(Resource, Clone, Copy)]
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct GizmoConfig {
|
||||
/// Set to `false` to stop drawing gizmos.
|
||||
///
|
||||
|
@ -113,6 +129,8 @@ pub struct GizmoConfig {
|
|||
///
|
||||
/// Defaults to `false`.
|
||||
pub on_top: bool,
|
||||
/// Configuration for the [`AabbGizmo`].
|
||||
pub aabb: AabbGizmoConfig,
|
||||
}
|
||||
|
||||
impl Default for GizmoConfig {
|
||||
|
@ -120,23 +138,90 @@ impl Default for GizmoConfig {
|
|||
Self {
|
||||
enabled: true,
|
||||
on_top: false,
|
||||
aabb: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for drawing the [`Aabb`] component on entities.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AabbGizmoConfig {
|
||||
/// Draws all bounding boxes in the scene when set to `true`.
|
||||
///
|
||||
/// To draw a specific entity's bounding box, you can add the [`AabbGizmo`] component.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
pub draw_all: bool,
|
||||
/// The default color for bounding box gizmos.
|
||||
///
|
||||
/// A random color is chosen per box if `None`.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub default_color: Option<Color>,
|
||||
}
|
||||
|
||||
/// Add this [`Component`] to an entity to draw its [`Aabb`] component.
|
||||
#[derive(Component, Reflect, FromReflect, Default, Debug)]
|
||||
#[reflect(Component, FromReflect, Default)]
|
||||
pub struct AabbGizmo {
|
||||
/// The color of the box.
|
||||
///
|
||||
/// The default color from the [`GizmoConfig`] resource is used if `None`,
|
||||
pub color: Option<Color>,
|
||||
}
|
||||
|
||||
fn draw_aabbs(
|
||||
query: Query<(Entity, &Aabb, &GlobalTransform, &AabbGizmo)>,
|
||||
config: Res<GizmoConfig>,
|
||||
mut gizmos: Gizmos,
|
||||
) {
|
||||
for (entity, &aabb, &transform, gizmo) in &query {
|
||||
let color = gizmo
|
||||
.color
|
||||
.or(config.aabb.default_color)
|
||||
.unwrap_or_else(|| color_from_entity(entity));
|
||||
gizmos.cuboid(aabb_transform(aabb, transform), color);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_all_aabbs(
|
||||
query: Query<(Entity, &Aabb, &GlobalTransform), Without<AabbGizmo>>,
|
||||
config: Res<GizmoConfig>,
|
||||
mut gizmos: Gizmos,
|
||||
) {
|
||||
for (entity, &aabb, &transform) in &query {
|
||||
let color = config
|
||||
.aabb
|
||||
.default_color
|
||||
.unwrap_or_else(|| color_from_entity(entity));
|
||||
gizmos.cuboid(aabb_transform(aabb, transform), color);
|
||||
}
|
||||
}
|
||||
|
||||
fn color_from_entity(entity: Entity) -> Color {
|
||||
let hue = entity.to_bits() as f32 * 100_000. % 360.;
|
||||
Color::hsl(hue, 1., 0.5)
|
||||
}
|
||||
|
||||
fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform {
|
||||
transform
|
||||
* GlobalTransform::from(
|
||||
Transform::from_translation(aabb.center.into())
|
||||
.with_scale((aabb.half_extents * 2.).into()),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct MeshHandles {
|
||||
list: Handle<Mesh>,
|
||||
strip: Handle<Mesh>,
|
||||
list: Option<Handle<Mesh>>,
|
||||
strip: Option<Handle<Mesh>>,
|
||||
}
|
||||
|
||||
impl FromWorld for MeshHandles {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let mut meshes = world.resource_mut::<Assets<Mesh>>();
|
||||
|
||||
fn from_world(_world: &mut World) -> Self {
|
||||
MeshHandles {
|
||||
list: meshes.add(Mesh::new(PrimitiveTopology::LineList)),
|
||||
strip: meshes.add(Mesh::new(PrimitiveTopology::LineStrip)),
|
||||
list: None,
|
||||
strip: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,24 +231,52 @@ struct GizmoMesh;
|
|||
|
||||
fn update_gizmo_meshes(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
handles: Res<MeshHandles>,
|
||||
mut handles: ResMut<MeshHandles>,
|
||||
mut storage: ResMut<GizmoStorage>,
|
||||
) {
|
||||
let list_mesh = meshes.get_mut(&handles.list).unwrap();
|
||||
if storage.list_positions.is_empty() {
|
||||
handles.list = None;
|
||||
} else if let Some(handle) = handles.list.as_ref() {
|
||||
let list_mesh = meshes.get_mut(handle).unwrap();
|
||||
|
||||
let positions = mem::take(&mut storage.list_positions);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
let positions = mem::take(&mut storage.list_positions);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
let colors = mem::take(&mut storage.list_colors);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
let colors = mem::take(&mut storage.list_colors);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
} else {
|
||||
let mut list_mesh = Mesh::new(PrimitiveTopology::LineList);
|
||||
|
||||
let strip_mesh = meshes.get_mut(&handles.strip).unwrap();
|
||||
let positions = mem::take(&mut storage.list_positions);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
let positions = mem::take(&mut storage.strip_positions);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
let colors = mem::take(&mut storage.list_colors);
|
||||
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
|
||||
let colors = mem::take(&mut storage.strip_colors);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
handles.list = Some(meshes.add(list_mesh));
|
||||
}
|
||||
|
||||
if storage.strip_positions.is_empty() {
|
||||
handles.strip = None;
|
||||
} else if let Some(handle) = handles.strip.as_ref() {
|
||||
let strip_mesh = meshes.get_mut(handle).unwrap();
|
||||
|
||||
let positions = mem::take(&mut storage.strip_positions);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
let colors = mem::take(&mut storage.strip_colors);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
} else {
|
||||
let mut strip_mesh = Mesh::new(PrimitiveTopology::LineStrip);
|
||||
|
||||
let positions = mem::take(&mut storage.strip_positions);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
let colors = mem::take(&mut storage.strip_colors);
|
||||
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
|
||||
handles.strip = Some(meshes.add(strip_mesh));
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_gizmo_data(
|
||||
|
@ -172,7 +285,7 @@ fn extract_gizmo_data(
|
|||
config: Extract<Res<GizmoConfig>>,
|
||||
) {
|
||||
if config.is_changed() {
|
||||
commands.insert_resource(**config);
|
||||
commands.insert_resource(config.clone());
|
||||
}
|
||||
|
||||
if !config.enabled {
|
||||
|
@ -181,28 +294,33 @@ fn extract_gizmo_data(
|
|||
|
||||
let transform = Mat4::IDENTITY;
|
||||
let inverse_transpose_model = transform.inverse().transpose();
|
||||
commands.spawn_batch([&handles.list, &handles.strip].map(|handle| {
|
||||
(
|
||||
GizmoMesh,
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
(
|
||||
handle.clone(),
|
||||
MeshUniform {
|
||||
flags: 0,
|
||||
transform,
|
||||
previous_transform: transform,
|
||||
inverse_transpose_model,
|
||||
},
|
||||
),
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
(
|
||||
Mesh2dHandle(handle.clone()),
|
||||
Mesh2dUniform {
|
||||
flags: 0,
|
||||
transform,
|
||||
inverse_transpose_model,
|
||||
},
|
||||
),
|
||||
)
|
||||
}));
|
||||
commands.spawn_batch(
|
||||
[handles.list.clone(), handles.strip.clone()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(move |handle| {
|
||||
(
|
||||
GizmoMesh,
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
(
|
||||
handle.clone_weak(),
|
||||
MeshUniform {
|
||||
flags: 0,
|
||||
transform,
|
||||
previous_transform: transform,
|
||||
inverse_transpose_model,
|
||||
},
|
||||
),
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
(
|
||||
Mesh2dHandle(handle),
|
||||
Mesh2dUniform {
|
||||
flags: 0,
|
||||
transform,
|
||||
inverse_transpose_model,
|
||||
},
|
||||
),
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use bevy_animation::AnimationClip;
|
|||
use bevy_utils::HashMap;
|
||||
|
||||
mod loader;
|
||||
mod vertex_attributes;
|
||||
pub use loader::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
|
@ -12,21 +13,47 @@ use bevy_asset::{AddAsset, Handle};
|
|||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
||||
use bevy_pbr::StandardMaterial;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_render::mesh::Mesh;
|
||||
use bevy_render::{
|
||||
mesh::{Mesh, MeshVertexAttribute},
|
||||
renderer::RenderDevice,
|
||||
texture::CompressedImageFormats,
|
||||
};
|
||||
use bevy_scene::Scene;
|
||||
|
||||
/// Adds support for glTF file loading to the app.
|
||||
#[derive(Default)]
|
||||
pub struct GltfPlugin;
|
||||
pub struct GltfPlugin {
|
||||
custom_vertex_attributes: HashMap<String, MeshVertexAttribute>,
|
||||
}
|
||||
|
||||
impl GltfPlugin {
|
||||
pub fn add_custom_vertex_attribute(
|
||||
mut self,
|
||||
name: &str,
|
||||
attribute: MeshVertexAttribute,
|
||||
) -> Self {
|
||||
self.custom_vertex_attributes
|
||||
.insert(name.to_string(), attribute);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for GltfPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset_loader::<GltfLoader>()
|
||||
.register_type::<GltfExtras>()
|
||||
.add_asset::<Gltf>()
|
||||
.add_asset::<GltfNode>()
|
||||
.add_asset::<GltfPrimitive>()
|
||||
.add_asset::<GltfMesh>();
|
||||
let supported_compressed_formats = match app.world.get_resource::<RenderDevice>() {
|
||||
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
|
||||
|
||||
None => CompressedImageFormats::all(),
|
||||
};
|
||||
app.add_asset_loader::<GltfLoader>(GltfLoader {
|
||||
supported_compressed_formats,
|
||||
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
|
||||
})
|
||||
.register_type::<GltfExtras>()
|
||||
.add_asset::<Gltf>()
|
||||
.add_asset::<GltfNode>()
|
||||
.add_asset::<GltfPrimitive>()
|
||||
.add_asset::<GltfMesh>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use bevy_asset::{
|
|||
};
|
||||
use bevy_core::Name;
|
||||
use bevy_core_pipeline::prelude::Camera3dBundle;
|
||||
use bevy_ecs::{entity::Entity, prelude::FromWorld, world::World};
|
||||
use bevy_ecs::{entity::Entity, world::World};
|
||||
use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder};
|
||||
use bevy_log::warn;
|
||||
use bevy_math::{Mat4, Vec3};
|
||||
|
@ -17,12 +17,11 @@ use bevy_render::{
|
|||
color::Color,
|
||||
mesh::{
|
||||
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
||||
Indices, Mesh, VertexAttributeValues,
|
||||
Indices, Mesh, MeshVertexAttribute, VertexAttributeValues,
|
||||
},
|
||||
prelude::SpatialBundle,
|
||||
primitives::Aabb,
|
||||
render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor},
|
||||
renderer::RenderDevice,
|
||||
texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError},
|
||||
};
|
||||
use bevy_scene::Scene;
|
||||
|
@ -32,13 +31,14 @@ use bevy_transform::components::Transform;
|
|||
|
||||
use bevy_utils::{HashMap, HashSet};
|
||||
use gltf::{
|
||||
mesh::Mode,
|
||||
mesh::{util::ReadIndices, Mode},
|
||||
texture::{MagFilter, MinFilter, WrappingMode},
|
||||
Material, Node, Primitive,
|
||||
};
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::vertex_attributes::*;
|
||||
use crate::{Gltf, GltfExtras, GltfNode};
|
||||
|
||||
/// An error that occurs when loading a glTF file.
|
||||
|
@ -68,7 +68,8 @@ pub enum GltfError {
|
|||
|
||||
/// Loads glTF files with all of their data as their corresponding bevy representations.
|
||||
pub struct GltfLoader {
|
||||
supported_compressed_formats: CompressedImageFormats,
|
||||
pub(crate) supported_compressed_formats: CompressedImageFormats,
|
||||
pub(crate) custom_vertex_attributes: HashMap<String, MeshVertexAttribute>,
|
||||
}
|
||||
|
||||
impl AssetLoader for GltfLoader {
|
||||
|
@ -77,9 +78,7 @@ impl AssetLoader for GltfLoader {
|
|||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
Box::pin(async move {
|
||||
Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?)
|
||||
})
|
||||
Box::pin(async move { Ok(load_gltf(bytes, load_context, self).await?) })
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
|
@ -87,24 +86,11 @@ impl AssetLoader for GltfLoader {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromWorld for GltfLoader {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let supported_compressed_formats = match world.get_resource::<RenderDevice>() {
|
||||
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
|
||||
|
||||
None => CompressedImageFormats::all(),
|
||||
};
|
||||
Self {
|
||||
supported_compressed_formats,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads an entire glTF file.
|
||||
async fn load_gltf<'a, 'b>(
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext<'b>,
|
||||
supported_compressed_formats: CompressedImageFormats,
|
||||
loader: &GltfLoader,
|
||||
) -> Result<(), GltfError> {
|
||||
let gltf = gltf::Gltf::from_slice(bytes)?;
|
||||
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
|
||||
|
@ -233,53 +219,31 @@ async fn load_gltf<'a, 'b>(
|
|||
let mut primitives = vec![];
|
||||
for primitive in mesh.primitives() {
|
||||
let primitive_label = primitive_label(&mesh, &primitive);
|
||||
let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()]));
|
||||
let primitive_topology = get_primitive_topology(primitive.mode())?;
|
||||
|
||||
let mut mesh = Mesh::new(primitive_topology);
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_positions()
|
||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_normals()
|
||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_tex_coords(0)
|
||||
.map(|v| VertexAttributeValues::Float32x2(v.into_f32().collect()))
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_colors(0)
|
||||
.map(|v| VertexAttributeValues::Float32x4(v.into_rgba_f32().collect()))
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(iter) = reader.read_joints(0) {
|
||||
let vertex_attribute = VertexAttributeValues::Uint16x4(iter.into_u16().collect());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_weights(0)
|
||||
.map(|v| VertexAttributeValues::Float32x4(v.into_f32().collect()))
|
||||
{
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, vertex_attribute);
|
||||
// Read vertex attributes
|
||||
for (semantic, accessor) in primitive.attributes() {
|
||||
match convert_attribute(
|
||||
semantic,
|
||||
accessor,
|
||||
&buffer_data,
|
||||
&loader.custom_vertex_attributes,
|
||||
) {
|
||||
Ok((attribute, values)) => mesh.insert_attribute(attribute, values),
|
||||
Err(err) => warn!("{}", err),
|
||||
}
|
||||
}
|
||||
|
||||
// Read vertex indices
|
||||
let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice()));
|
||||
if let Some(indices) = reader.read_indices() {
|
||||
mesh.set_indices(Some(Indices::U32(indices.into_u32().collect())));
|
||||
mesh.set_indices(Some(match indices {
|
||||
ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()),
|
||||
ReadIndices::U16(is) => Indices::U16(is.collect()),
|
||||
ReadIndices::U32(is) => Indices::U32(is.collect()),
|
||||
}));
|
||||
};
|
||||
|
||||
if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none()
|
||||
|
@ -403,7 +367,7 @@ async fn load_gltf<'a, 'b>(
|
|||
&buffer_data,
|
||||
&linear_textures,
|
||||
load_context,
|
||||
supported_compressed_formats,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await?;
|
||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||
|
@ -422,7 +386,7 @@ async fn load_gltf<'a, 'b>(
|
|||
buffer_data,
|
||||
linear_textures,
|
||||
load_context,
|
||||
supported_compressed_formats,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
|
287
crates/bevy_gltf/src/vertex_attributes.rs
Normal file
287
crates/bevy_gltf/src/vertex_attributes.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use bevy_render::{
|
||||
mesh::{MeshVertexAttribute, VertexAttributeValues as Values},
|
||||
prelude::Mesh,
|
||||
render_resource::VertexFormat,
|
||||
};
|
||||
use bevy_utils::HashMap;
|
||||
use gltf::{
|
||||
accessor::{DataType, Dimensions},
|
||||
mesh::util::{ReadColors, ReadJoints, ReadTexCoords},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Represents whether integer data requires normalization
|
||||
#[derive(Copy, Clone)]
|
||||
struct Normalization(bool);
|
||||
|
||||
impl Normalization {
|
||||
fn apply_either<T, U>(
|
||||
self,
|
||||
value: T,
|
||||
normalized_ctor: impl Fn(T) -> U,
|
||||
unnormalized_ctor: impl Fn(T) -> U,
|
||||
) -> U {
|
||||
if self.0 {
|
||||
normalized_ctor(value)
|
||||
} else {
|
||||
unnormalized_ctor(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occurs when accessing buffer data
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum AccessFailed {
|
||||
#[error("Malformed vertex attribute data")]
|
||||
MalformedData,
|
||||
#[error("Unsupported vertex attribute format")]
|
||||
UnsupportedFormat,
|
||||
}
|
||||
|
||||
/// Helper for reading buffer data
|
||||
struct BufferAccessor<'a> {
|
||||
accessor: gltf::Accessor<'a>,
|
||||
buffer_data: &'a Vec<Vec<u8>>,
|
||||
normalization: Normalization,
|
||||
}
|
||||
|
||||
impl<'a> BufferAccessor<'a> {
|
||||
/// Creates an iterator over the elements in this accessor
|
||||
fn iter<T: gltf::accessor::Item>(self) -> Result<gltf::accessor::Iter<'a, T>, AccessFailed> {
|
||||
gltf::accessor::Iter::new(self.accessor, |buffer: gltf::Buffer| {
|
||||
self.buffer_data.get(buffer.index()).map(|v| v.as_slice())
|
||||
})
|
||||
.ok_or(AccessFailed::MalformedData)
|
||||
}
|
||||
|
||||
/// Applies the element iterator to a constructor or fails if normalization is required
|
||||
fn with_no_norm<T: gltf::accessor::Item, U>(
|
||||
self,
|
||||
ctor: impl Fn(gltf::accessor::Iter<'a, T>) -> U,
|
||||
) -> Result<U, AccessFailed> {
|
||||
if self.normalization.0 {
|
||||
return Err(AccessFailed::UnsupportedFormat);
|
||||
}
|
||||
self.iter().map(ctor)
|
||||
}
|
||||
|
||||
/// Applies the element iterator and the normalization flag to a constructor
|
||||
fn with_norm<T: gltf::accessor::Item, U>(
|
||||
self,
|
||||
ctor: impl Fn(gltf::accessor::Iter<'a, T>, Normalization) -> U,
|
||||
) -> Result<U, AccessFailed> {
|
||||
let normalized = self.normalization;
|
||||
self.iter().map(|v| ctor(v, normalized))
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum of the iterators user by different vertex attribute formats
|
||||
enum VertexAttributeIter<'a> {
|
||||
// For reading native WGPU formats
|
||||
F32(gltf::accessor::Iter<'a, f32>),
|
||||
U32(gltf::accessor::Iter<'a, u32>),
|
||||
F32x2(gltf::accessor::Iter<'a, [f32; 2]>),
|
||||
U32x2(gltf::accessor::Iter<'a, [u32; 2]>),
|
||||
F32x3(gltf::accessor::Iter<'a, [f32; 3]>),
|
||||
U32x3(gltf::accessor::Iter<'a, [u32; 3]>),
|
||||
F32x4(gltf::accessor::Iter<'a, [f32; 4]>),
|
||||
U32x4(gltf::accessor::Iter<'a, [u32; 4]>),
|
||||
S16x2(gltf::accessor::Iter<'a, [i16; 2]>, Normalization),
|
||||
U16x2(gltf::accessor::Iter<'a, [u16; 2]>, Normalization),
|
||||
S16x4(gltf::accessor::Iter<'a, [i16; 4]>, Normalization),
|
||||
U16x4(gltf::accessor::Iter<'a, [u16; 4]>, Normalization),
|
||||
S8x2(gltf::accessor::Iter<'a, [i8; 2]>, Normalization),
|
||||
U8x2(gltf::accessor::Iter<'a, [u8; 2]>, Normalization),
|
||||
S8x4(gltf::accessor::Iter<'a, [i8; 4]>, Normalization),
|
||||
U8x4(gltf::accessor::Iter<'a, [u8; 4]>, Normalization),
|
||||
// Additional on-disk formats used for RGB colors
|
||||
U16x3(gltf::accessor::Iter<'a, [u16; 3]>, Normalization),
|
||||
U8x3(gltf::accessor::Iter<'a, [u8; 3]>, Normalization),
|
||||
}
|
||||
|
||||
impl<'a> VertexAttributeIter<'a> {
|
||||
/// Creates an iterator over the elements in a vertex attribute accessor
|
||||
fn from_accessor(
|
||||
accessor: gltf::Accessor<'a>,
|
||||
buffer_data: &'a Vec<Vec<u8>>,
|
||||
) -> Result<VertexAttributeIter<'a>, AccessFailed> {
|
||||
let normalization = Normalization(accessor.normalized());
|
||||
let format = (accessor.data_type(), accessor.dimensions());
|
||||
let acc = BufferAccessor {
|
||||
accessor,
|
||||
buffer_data,
|
||||
normalization,
|
||||
};
|
||||
match format {
|
||||
(DataType::F32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::F32),
|
||||
(DataType::U32, Dimensions::Scalar) => acc.with_no_norm(VertexAttributeIter::U32),
|
||||
(DataType::F32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::F32x2),
|
||||
(DataType::U32, Dimensions::Vec2) => acc.with_no_norm(VertexAttributeIter::U32x2),
|
||||
(DataType::F32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::F32x3),
|
||||
(DataType::U32, Dimensions::Vec3) => acc.with_no_norm(VertexAttributeIter::U32x3),
|
||||
(DataType::F32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::F32x4),
|
||||
(DataType::U32, Dimensions::Vec4) => acc.with_no_norm(VertexAttributeIter::U32x4),
|
||||
(DataType::I16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S16x2),
|
||||
(DataType::U16, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U16x2),
|
||||
(DataType::I16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S16x4),
|
||||
(DataType::U16, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U16x4),
|
||||
(DataType::I8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::S8x2),
|
||||
(DataType::U8, Dimensions::Vec2) => acc.with_norm(VertexAttributeIter::U8x2),
|
||||
(DataType::I8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::S8x4),
|
||||
(DataType::U8, Dimensions::Vec4) => acc.with_norm(VertexAttributeIter::U8x4),
|
||||
(DataType::U16, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U16x3),
|
||||
(DataType::U8, Dimensions::Vec3) => acc.with_norm(VertexAttributeIter::U8x3),
|
||||
_ => Err(AccessFailed::UnsupportedFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes values for any supported format of vertex attribute
|
||||
fn into_any_values(self) -> Result<Values, AccessFailed> {
|
||||
match self {
|
||||
VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())),
|
||||
VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())),
|
||||
VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())),
|
||||
VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())),
|
||||
VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())),
|
||||
VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())),
|
||||
VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())),
|
||||
VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())),
|
||||
VertexAttributeIter::S16x2(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2))
|
||||
}
|
||||
VertexAttributeIter::U16x2(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Unorm16x2, Values::Uint16x2))
|
||||
}
|
||||
VertexAttributeIter::S16x4(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Snorm16x4, Values::Sint16x4))
|
||||
}
|
||||
VertexAttributeIter::U16x4(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Unorm16x4, Values::Uint16x4))
|
||||
}
|
||||
VertexAttributeIter::S8x2(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Snorm8x2, Values::Sint8x2))
|
||||
}
|
||||
VertexAttributeIter::U8x2(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Unorm8x2, Values::Uint8x2))
|
||||
}
|
||||
VertexAttributeIter::S8x4(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Snorm8x4, Values::Sint8x4))
|
||||
}
|
||||
VertexAttributeIter::U8x4(it, n) => {
|
||||
Ok(n.apply_either(it.collect(), Values::Unorm8x4, Values::Uint8x4))
|
||||
}
|
||||
_ => Err(AccessFailed::UnsupportedFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes RGBA values, converting compatible formats to Float32x4
|
||||
fn into_rgba_values(self) -> Result<Values, AccessFailed> {
|
||||
match self {
|
||||
VertexAttributeIter::U8x3(it, Normalization(true)) => Ok(Values::Float32x4(
|
||||
ReadColors::RgbU8(it).into_rgba_f32().collect(),
|
||||
)),
|
||||
VertexAttributeIter::U16x3(it, Normalization(true)) => Ok(Values::Float32x4(
|
||||
ReadColors::RgbU16(it).into_rgba_f32().collect(),
|
||||
)),
|
||||
VertexAttributeIter::F32x3(it) => Ok(Values::Float32x4(
|
||||
ReadColors::RgbF32(it).into_rgba_f32().collect(),
|
||||
)),
|
||||
VertexAttributeIter::U8x4(it, Normalization(true)) => Ok(Values::Float32x4(
|
||||
ReadColors::RgbaU8(it).into_rgba_f32().collect(),
|
||||
)),
|
||||
VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4(
|
||||
ReadColors::RgbaU16(it).into_rgba_f32().collect(),
|
||||
)),
|
||||
s => s.into_any_values(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes joint index values, converting compatible formats to Uint16x4
|
||||
fn into_joint_index_values(self) -> Result<Values, AccessFailed> {
|
||||
match self {
|
||||
VertexAttributeIter::U8x4(it, Normalization(false)) => {
|
||||
Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect()))
|
||||
}
|
||||
s => s.into_any_values(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes texture coordinate values, converting compatible formats to Float32x2
|
||||
fn into_tex_coord_values(self) -> Result<Values, AccessFailed> {
|
||||
match self {
|
||||
VertexAttributeIter::U8x2(it, Normalization(true)) => Ok(Values::Float32x2(
|
||||
ReadTexCoords::U8(it).into_f32().collect(),
|
||||
)),
|
||||
VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2(
|
||||
ReadTexCoords::U16(it).into_f32().collect(),
|
||||
)),
|
||||
s => s.into_any_values(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConversionMode {
|
||||
Any,
|
||||
Rgba,
|
||||
JointIndex,
|
||||
TexCoord,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum ConvertAttributeError {
|
||||
#[error("Vertex attribute {0} has format {1:?} but expected {3:?} for target attribute {2}")]
|
||||
WrongFormat(String, VertexFormat, String, VertexFormat),
|
||||
#[error("{0} in accessor {1}")]
|
||||
AccessFailed(AccessFailed, usize),
|
||||
#[error("Unknown vertex attribute {0}")]
|
||||
UnknownName(String),
|
||||
}
|
||||
|
||||
pub(crate) fn convert_attribute(
|
||||
semantic: gltf::Semantic,
|
||||
accessor: gltf::Accessor,
|
||||
buffer_data: &Vec<Vec<u8>>,
|
||||
custom_vertex_attributes: &HashMap<String, MeshVertexAttribute>,
|
||||
) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> {
|
||||
if let Some((attribute, conversion)) = match &semantic {
|
||||
gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)),
|
||||
gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)),
|
||||
gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)),
|
||||
gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)),
|
||||
gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)),
|
||||
gltf::Semantic::Joints(0) => {
|
||||
Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex))
|
||||
}
|
||||
gltf::Semantic::Weights(0) => Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::Any)),
|
||||
gltf::Semantic::Extras(name) => custom_vertex_attributes
|
||||
.get(name)
|
||||
.map(|attr| (attr.clone(), ConversionMode::Any)),
|
||||
_ => None,
|
||||
} {
|
||||
let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data);
|
||||
let converted_values = raw_iter.and_then(|iter| match conversion {
|
||||
ConversionMode::Any => iter.into_any_values(),
|
||||
ConversionMode::Rgba => iter.into_rgba_values(),
|
||||
ConversionMode::TexCoord => iter.into_tex_coord_values(),
|
||||
ConversionMode::JointIndex => iter.into_joint_index_values(),
|
||||
});
|
||||
match converted_values {
|
||||
Ok(values) => {
|
||||
let loaded_format = VertexFormat::from(&values);
|
||||
if attribute.format == loaded_format {
|
||||
Ok((attribute, values))
|
||||
} else {
|
||||
Err(ConvertAttributeError::WrongFormat(
|
||||
semantic.to_string(),
|
||||
loaded_format,
|
||||
attribute.name.to_string(),
|
||||
attribute.format,
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(ConvertAttributeError::AccessFailed(err, accessor.index())),
|
||||
}
|
||||
} else {
|
||||
Err(ConvertAttributeError::UnknownName(semantic.to_string()))
|
||||
}
|
||||
}
|
|
@ -16,6 +16,9 @@ fn push_events(world: &mut World, events: impl IntoIterator<Item = HierarchyEven
|
|||
}
|
||||
}
|
||||
|
||||
/// Adds `child` to `parent`'s [`Children`], without checking if it is already present there.
|
||||
///
|
||||
/// This might cause unexpected results when removing duplicate children.
|
||||
fn push_child_unchecked(world: &mut World, parent: Entity, child: Entity) {
|
||||
let mut parent = world.entity_mut(parent);
|
||||
if let Some(mut children) = parent.get_mut::<Children>() {
|
||||
|
@ -25,6 +28,7 @@ fn push_child_unchecked(world: &mut World, parent: Entity, child: Entity) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets [`Parent`] of the `child` to `new_parent`. Inserts [`Parent`] if `child` doesn't have one.
|
||||
fn update_parent(world: &mut World, child: Entity, new_parent: Entity) -> Option<Entity> {
|
||||
let mut child = world.entity_mut(child);
|
||||
if let Some(mut parent) = child.get_mut::<Parent>() {
|
||||
|
@ -41,12 +45,15 @@ fn update_parent(world: &mut World, child: Entity, new_parent: Entity) -> Option
|
|||
///
|
||||
/// Removes the [`Children`] component from the parent if it's empty.
|
||||
fn remove_from_children(world: &mut World, parent: Entity, child: Entity) {
|
||||
let mut parent = world.entity_mut(parent);
|
||||
if let Some(mut children) = parent.get_mut::<Children>() {
|
||||
children.0.retain(|x| *x != child);
|
||||
if children.is_empty() {
|
||||
parent.remove::<Children>();
|
||||
}
|
||||
let Some(mut parent) = world.get_entity_mut(parent) else {
|
||||
return;
|
||||
};
|
||||
let Some(mut children) = parent.get_mut::<Children>() else {
|
||||
return;
|
||||
};
|
||||
children.0.retain(|x| *x != child);
|
||||
if children.is_empty() {
|
||||
parent.remove::<Children>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +117,8 @@ fn update_old_parents(world: &mut World, parent: Entity, children: &[Entity]) {
|
|||
push_events(world, events);
|
||||
}
|
||||
|
||||
/// Removes entities in `children` from `parent`'s [`Children`], removing the component if it ends up empty.
|
||||
/// Also removes [`Parent`] component from `children`.
|
||||
fn remove_children(parent: Entity, children: &[Entity], world: &mut World) {
|
||||
let mut events: SmallVec<[HierarchyEvent; 8]> = SmallVec::new();
|
||||
if let Some(parent_children) = world.get::<Children>(parent) {
|
||||
|
@ -140,6 +149,8 @@ fn remove_children(parent: Entity, children: &[Entity], world: &mut World) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Removes all children from `parent` by removing its [`Children`] component, as well as removing
|
||||
/// [`Parent`] component from its children.
|
||||
fn clear_children(parent: Entity, world: &mut World) {
|
||||
if let Some(children) = world.entity_mut(parent).take::<Children>() {
|
||||
for &child in &children.0 {
|
||||
|
@ -148,12 +159,12 @@ fn clear_children(parent: Entity, world: &mut World) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Command that adds a child to an entity
|
||||
/// Command that adds a child to an entity.
|
||||
#[derive(Debug)]
|
||||
pub struct AddChild {
|
||||
/// Parent entity to add the child to
|
||||
/// Parent entity to add the child to.
|
||||
pub parent: Entity,
|
||||
/// Child entity to add
|
||||
/// Child entity to add.
|
||||
pub child: Entity,
|
||||
}
|
||||
|
||||
|
@ -163,7 +174,7 @@ impl Command for AddChild {
|
|||
}
|
||||
}
|
||||
|
||||
/// Command that inserts a child at a given index of a parent's children, shifting following children back
|
||||
/// Command that inserts a child at a given index of a parent's children, shifting following children back.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertChildren {
|
||||
parent: Entity,
|
||||
|
@ -192,7 +203,7 @@ impl Command for PushChildren {
|
|||
}
|
||||
}
|
||||
|
||||
/// Command that removes children from an entity, and removes that child's parent.
|
||||
/// Command that removes children from an entity, and removes these children's parent.
|
||||
pub struct RemoveChildren {
|
||||
parent: Entity,
|
||||
children: SmallVec<[Entity; 8]>,
|
||||
|
@ -204,7 +215,8 @@ impl Command for RemoveChildren {
|
|||
}
|
||||
}
|
||||
|
||||
/// Command that clear all children from an entity.
|
||||
/// Command that clears all children from an entity and removes [`Parent`] component from those
|
||||
/// children.
|
||||
pub struct ClearChildren {
|
||||
parent: Entity,
|
||||
}
|
||||
|
@ -215,7 +227,7 @@ impl Command for ClearChildren {
|
|||
}
|
||||
}
|
||||
|
||||
/// Command that clear all children from an entity. And replace with the given children.
|
||||
/// Command that clear all children from an entity, replacing them with the given children.
|
||||
pub struct ReplaceChildren {
|
||||
parent: Entity,
|
||||
children: SmallVec<[Entity; 8]>,
|
||||
|
@ -240,42 +252,44 @@ impl Command for RemoveParent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Struct for building children onto an entity
|
||||
/// Struct for building children entities and adding them to a parent entity.
|
||||
pub struct ChildBuilder<'w, 's, 'a> {
|
||||
commands: &'a mut Commands<'w, 's>,
|
||||
push_children: PushChildren,
|
||||
}
|
||||
|
||||
impl<'w, 's, 'a> ChildBuilder<'w, 's, 'a> {
|
||||
/// Spawns an entity with the given bundle and inserts it into the children defined by the [`ChildBuilder`]
|
||||
/// Spawns an entity with the given bundle and inserts it into the parent entity's [`Children`].
|
||||
/// Also adds [`Parent`] component to the created entity.
|
||||
pub fn spawn(&mut self, bundle: impl Bundle) -> EntityCommands<'w, 's, '_> {
|
||||
let e = self.commands.spawn(bundle);
|
||||
self.push_children.children.push(e.id());
|
||||
e
|
||||
}
|
||||
|
||||
/// Spawns an [`Entity`] with no components and inserts it into the children defined by the [`ChildBuilder`] which adds the [`Parent`] component to it.
|
||||
/// Spawns an [`Entity`] with no components and inserts it into the parent entity's [`Children`].
|
||||
/// Also adds [`Parent`] component to the created entity.
|
||||
pub fn spawn_empty(&mut self) -> EntityCommands<'w, 's, '_> {
|
||||
let e = self.commands.spawn_empty();
|
||||
self.push_children.children.push(e.id());
|
||||
e
|
||||
}
|
||||
|
||||
/// Returns the parent entity of this [`ChildBuilder`]
|
||||
/// Returns the parent entity of this [`ChildBuilder`].
|
||||
pub fn parent_entity(&self) -> Entity {
|
||||
self.push_children.parent
|
||||
}
|
||||
|
||||
/// Adds a command to this [`ChildBuilder`]
|
||||
/// Adds a command to be executed, like [`Commands::add`].
|
||||
pub fn add_command<C: Command + 'static>(&mut self, command: C) -> &mut Self {
|
||||
self.commands.add(command);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait defining how to build children
|
||||
/// Trait for removing, adding and replacing children and parents of an entity.
|
||||
pub trait BuildChildren {
|
||||
/// Creates a [`ChildBuilder`] with the given children built in the given closure
|
||||
/// Takes a clousre which builds children for this entity using [`ChildBuilder`].
|
||||
fn with_children(&mut self, f: impl FnOnce(&mut ChildBuilder)) -> &mut Self;
|
||||
/// Pushes children to the back of the builder's children. For any entities that are
|
||||
/// already a child of this one, this method does nothing.
|
||||
|
@ -284,7 +298,7 @@ pub trait BuildChildren {
|
|||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
/// [`Children`] component to be removed from the entity.
|
||||
fn push_children(&mut self, children: &[Entity]) -> &mut Self;
|
||||
/// Inserts children at the given index
|
||||
/// Inserts children at the given index.
|
||||
///
|
||||
/// If the children were previously children of another parent, that parent's [`Children`] component
|
||||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
|
@ -294,7 +308,7 @@ pub trait BuildChildren {
|
|||
///
|
||||
/// Removing all children from a parent causes its [`Children`] component to be removed from the entity.
|
||||
fn remove_children(&mut self, children: &[Entity]) -> &mut Self;
|
||||
/// Adds a single child
|
||||
/// Adds a single child.
|
||||
///
|
||||
/// If the children were previously children of another parent, that parent's [`Children`] component
|
||||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
|
@ -303,10 +317,19 @@ pub trait BuildChildren {
|
|||
/// Removes all children from this entity. The [`Children`] component will be removed if it exists, otherwise this does nothing.
|
||||
fn clear_children(&mut self) -> &mut Self;
|
||||
/// Removes all current children from this entity, replacing them with the specified list of entities.
|
||||
///
|
||||
/// The removed children will have their [`Parent`] component removed.
|
||||
fn replace_children(&mut self, children: &[Entity]) -> &mut Self;
|
||||
/// Sets the parent of this entity.
|
||||
///
|
||||
/// If this entity already had a parent, the parent's [`Children`] component will have this
|
||||
/// child removed from its list. Removing all children from a parent causes its [`Children`]
|
||||
/// component to be removed from the entity.
|
||||
fn set_parent(&mut self, parent: Entity) -> &mut Self;
|
||||
/// Removes the parent of this entity.
|
||||
/// Removes the [`Parent`] of this entity.
|
||||
///
|
||||
/// Also removes this entity from its parent's [`Children`] component. Removing all children from a parent causes
|
||||
/// its [`Children`] component to be removed from the entity.
|
||||
fn remove_parent(&mut self) -> &mut Self;
|
||||
}
|
||||
|
||||
|
@ -389,7 +412,7 @@ impl<'w, 's, 'a> BuildChildren for EntityCommands<'w, 's, 'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Struct for adding children to an entity directly through the [`World`] for use in exclusive systems
|
||||
/// Struct for adding children to an entity directly through the [`World`] for use in exclusive systems.
|
||||
#[derive(Debug)]
|
||||
pub struct WorldChildBuilder<'w> {
|
||||
world: &'w mut World,
|
||||
|
@ -397,7 +420,8 @@ pub struct WorldChildBuilder<'w> {
|
|||
}
|
||||
|
||||
impl<'w> WorldChildBuilder<'w> {
|
||||
/// Spawns an entity with the given bundle and inserts it into the children defined by the [`WorldChildBuilder`]
|
||||
/// Spawns an entity with the given bundle and inserts it into the parent entity's [`Children`].
|
||||
/// Also adds [`Parent`] component to the created entity.
|
||||
pub fn spawn(&mut self, bundle: impl Bundle + Send + Sync + 'static) -> EntityMut<'_> {
|
||||
let entity = self.world.spawn((bundle, Parent(self.parent))).id();
|
||||
push_child_unchecked(self.world, self.parent, entity);
|
||||
|
@ -411,7 +435,8 @@ impl<'w> WorldChildBuilder<'w> {
|
|||
self.world.entity_mut(entity)
|
||||
}
|
||||
|
||||
/// Spawns an [`Entity`] with no components and inserts it into the children defined by the [`WorldChildBuilder`] which adds the [`Parent`] component to it.
|
||||
/// Spawns an [`Entity`] with no components and inserts it into the parent entity's [`Children`].
|
||||
/// Also adds [`Parent`] component to the created entity.
|
||||
pub fn spawn_empty(&mut self) -> EntityMut<'_> {
|
||||
let entity = self.world.spawn(Parent(self.parent)).id();
|
||||
push_child_unchecked(self.world, self.parent, entity);
|
||||
|
@ -425,37 +450,53 @@ impl<'w> WorldChildBuilder<'w> {
|
|||
self.world.entity_mut(entity)
|
||||
}
|
||||
|
||||
/// Returns the parent entity of this [`WorldChildBuilder`]
|
||||
/// Returns the parent entity of this [`WorldChildBuilder`].
|
||||
pub fn parent_entity(&self) -> Entity {
|
||||
self.parent
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that defines adding children to an entity directly through the [`World`]
|
||||
/// Trait that defines adding, changing and children and parents of an entity directly through the [`World`].
|
||||
pub trait BuildWorldChildren {
|
||||
/// Creates a [`WorldChildBuilder`] with the given children built in the given closure
|
||||
/// Takes a clousre which builds children for this entity using [`WorldChildBuilder`].
|
||||
fn with_children(&mut self, spawn_children: impl FnOnce(&mut WorldChildBuilder)) -> &mut Self;
|
||||
|
||||
/// Adds a single child
|
||||
/// Adds a single child.
|
||||
///
|
||||
/// If the children were previously children of another parent, that parent's [`Children`] component
|
||||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
/// [`Children`] component to be removed from the entity.
|
||||
fn add_child(&mut self, child: Entity) -> &mut Self;
|
||||
|
||||
/// Pushes children to the back of the builder's children
|
||||
/// Pushes children to the back of the builder's children. For any entities that are
|
||||
/// already a child of this one, this method does nothing.
|
||||
///
|
||||
/// If the children were previously children of another parent, that parent's [`Children`] component
|
||||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
/// [`Children`] component to be removed from the entity.
|
||||
fn push_children(&mut self, children: &[Entity]) -> &mut Self;
|
||||
/// Inserts children at the given index
|
||||
/// Inserts children at the given index.
|
||||
///
|
||||
/// If the children were previously children of another parent, that parent's [`Children`] component
|
||||
/// will have those children removed from its list. Removing all children from a parent causes its
|
||||
/// [`Children`] component to be removed from the entity.
|
||||
fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self;
|
||||
/// Removes the given children
|
||||
///
|
||||
/// Removing all children from a parent causes its [`Children`] component to be removed from the entity.
|
||||
fn remove_children(&mut self, children: &[Entity]) -> &mut Self;
|
||||
|
||||
/// Set the `parent` of this entity. This entity will be added to the end of the `parent`'s list of children.
|
||||
/// Sets the parent of this entity.
|
||||
///
|
||||
/// If this entity already had a parent it will be removed from it.
|
||||
/// If this entity already had a parent, the parent's [`Children`] component will have this
|
||||
/// child removed from its list. Removing all children from a parent causes its [`Children`]
|
||||
/// component to be removed from the entity.
|
||||
fn set_parent(&mut self, parent: Entity) -> &mut Self;
|
||||
|
||||
/// Remove the parent from this entity.
|
||||
/// Removes the [`Parent`] of this entity.
|
||||
///
|
||||
/// Also removes this entity from its parent's [`Children`] component. Removing all children from a parent causes
|
||||
/// its [`Children`] component to be removed from the entity.
|
||||
fn remove_parent(&mut self) -> &mut Self;
|
||||
}
|
||||
|
||||
|
@ -653,6 +694,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// regression test for https://github.com/bevyengine/bevy/pull/8346
|
||||
#[test]
|
||||
fn set_parent_of_orphan() {
|
||||
let world = &mut World::new();
|
||||
|
||||
let [a, b, c] = std::array::from_fn(|_| world.spawn_empty().id());
|
||||
world.entity_mut(a).set_parent(b);
|
||||
assert_parent(world, a, Some(b));
|
||||
assert_children(world, b, Some(&[a]));
|
||||
|
||||
world.entity_mut(b).despawn();
|
||||
world.entity_mut(a).set_parent(c);
|
||||
|
||||
assert_parent(world, a, Some(c));
|
||||
assert_children(world, c, Some(&[a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_parent() {
|
||||
let world = &mut World::new();
|
||||
|
|
|
@ -46,9 +46,9 @@ fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity) {
|
|||
}
|
||||
}
|
||||
|
||||
fn despawn_children(world: &mut World, entity: Entity) {
|
||||
if let Some(mut children) = world.get_mut::<Children>(entity) {
|
||||
for e in std::mem::take(&mut children.0) {
|
||||
fn despawn_children_recursive(world: &mut World, entity: Entity) {
|
||||
if let Some(children) = world.entity_mut(entity).take::<Children>() {
|
||||
for e in children.0 {
|
||||
despawn_with_children_recursive_inner(world, e);
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ impl Command for DespawnChildrenRecursive {
|
|||
entity = bevy_utils::tracing::field::debug(self.entity)
|
||||
)
|
||||
.entered();
|
||||
despawn_children(world, self.entity);
|
||||
despawn_children_recursive(world, self.entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,11 +127,9 @@ impl<'w> DespawnRecursiveExt for EntityMut<'w> {
|
|||
)
|
||||
.entered();
|
||||
|
||||
// SAFETY: The location is updated.
|
||||
unsafe {
|
||||
despawn_children(self.world_mut(), entity);
|
||||
self.update_location();
|
||||
}
|
||||
self.world_scope(|world| {
|
||||
despawn_children_recursive(world, entity);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,4 +224,26 @@ mod tests {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn despawn_descendants() {
|
||||
let mut world = World::default();
|
||||
let mut queue = CommandQueue::default();
|
||||
let mut commands = Commands::new(&mut queue, &world);
|
||||
|
||||
let parent = commands.spawn_empty().id();
|
||||
let child = commands.spawn_empty().id();
|
||||
|
||||
commands
|
||||
.entity(parent)
|
||||
.add_child(child)
|
||||
.despawn_descendants();
|
||||
|
||||
queue.apply(&mut world);
|
||||
|
||||
// The parent's Children component should be removed.
|
||||
assert!(world.entity(parent).get::<Children>().is_none());
|
||||
// The child should be despawned.
|
||||
assert!(world.get_entity(child).is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ impl<T> Default for ReportHierarchyIssue<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// System to print a warning for each `Entity` with a `T` component
|
||||
/// System to print a warning for each [`Entity`] with a `T` component
|
||||
/// which parent hasn't a `T` component.
|
||||
///
|
||||
/// Hierarchy propagations are top-down, and limited only to entities
|
||||
|
|
|
@ -5,6 +5,7 @@ use bevy_ecs::{
|
|||
system::{Res, ResMut, Resource},
|
||||
};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
|
||||
use bevy_utils::Duration;
|
||||
use bevy_utils::{tracing::info, HashMap};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -90,7 +91,7 @@ impl Gamepad {
|
|||
}
|
||||
}
|
||||
|
||||
/// Metadata associated with a `Gamepad`.
|
||||
/// Metadata associated with a [`Gamepad`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)]
|
||||
#[reflect(Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
|
@ -629,9 +630,9 @@ impl AxisSettings {
|
|||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an `AxisSettingsError` if any restrictions on the zone values are not met.
|
||||
/// If the zone restrictions are met, but the ``threshold`` value restrictions are not met,
|
||||
/// returns `AxisSettingsError::Threshold`.
|
||||
/// Returns an [`AxisSettingsError`] if any restrictions on the zone values are not met.
|
||||
/// If the zone restrictions are met, but the `threshold` value restrictions are not met,
|
||||
/// returns [`AxisSettingsError::Threshold`].
|
||||
pub fn new(
|
||||
livezone_lowerbound: f32,
|
||||
deadzone_lowerbound: f32,
|
||||
|
@ -875,7 +876,7 @@ impl AxisSettings {
|
|||
}
|
||||
|
||||
/// Determines whether the change from `old_value` to `new_value` should
|
||||
/// be registered as a change, according to the `AxisSettings`.
|
||||
/// be registered as a change, according to the [`AxisSettings`].
|
||||
fn should_register_change(&self, new_value: f32, old_value: Option<f32>) -> bool {
|
||||
if old_value.is_none() {
|
||||
return true;
|
||||
|
@ -963,7 +964,7 @@ impl ButtonAxisSettings {
|
|||
f32::abs(new_value - old_value.unwrap()) > self.threshold
|
||||
}
|
||||
|
||||
/// Filters the `new_value` based on the `old_value`, according to the `ButtonAxisSettings`.
|
||||
/// Filters the `new_value` based on the `old_value`, according to the [`ButtonAxisSettings`].
|
||||
///
|
||||
/// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change
|
||||
/// exceeds the settings threshold, and `None` otherwise.
|
||||
|
@ -1116,7 +1117,7 @@ impl GamepadButtonChangedEvent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Uses [`GamepadAxisChangedEvent`]s to update the relevant `Input` and `Axis` values.
|
||||
/// Uses [`GamepadAxisChangedEvent`]s to update the relevant [`Input`] and [`Axis`] values.
|
||||
pub fn gamepad_axis_event_system(
|
||||
mut gamepad_axis: ResMut<Axis<GamepadAxis>>,
|
||||
mut axis_events: EventReader<GamepadAxisChangedEvent>,
|
||||
|
@ -1127,7 +1128,7 @@ pub fn gamepad_axis_event_system(
|
|||
}
|
||||
}
|
||||
|
||||
/// Uses [`GamepadButtonChangedEvent`]s to update the relevant `Input` and `Axis` values.
|
||||
/// Uses [`GamepadButtonChangedEvent`]s to update the relevant [`Input`] and [`Axis`] values.
|
||||
pub fn gamepad_button_event_system(
|
||||
mut button_events: EventReader<GamepadButtonChangedEvent>,
|
||||
mut button_input: ResMut<Input<GamepadButton>>,
|
||||
|
@ -1240,6 +1241,127 @@ const ALL_AXIS_TYPES: [GamepadAxisType; 6] = [
|
|||
GamepadAxisType::RightZ,
|
||||
];
|
||||
|
||||
/// The intensity at which a gamepad's force-feedback motors may rumble.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct GamepadRumbleIntensity {
|
||||
/// The rumble intensity of the strong gamepad motor
|
||||
///
|
||||
/// Ranges from 0.0 to 1.0
|
||||
///
|
||||
/// By convention, this is usually a low-frequency motor on the left-hand
|
||||
/// side of the gamepad, though it may vary across platforms and hardware.
|
||||
pub strong_motor: f32,
|
||||
/// The rumble intensity of the weak gamepad motor
|
||||
///
|
||||
/// Ranges from 0.0 to 1.0
|
||||
///
|
||||
/// By convention, this is usually a high-frequency motor on the right-hand
|
||||
/// side of the gamepad, though it may vary across platforms and hardware.
|
||||
pub weak_motor: f32,
|
||||
}
|
||||
|
||||
impl GamepadRumbleIntensity {
|
||||
/// Rumble both gamepad motors at maximum intensity
|
||||
pub const MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 1.0,
|
||||
weak_motor: 1.0,
|
||||
};
|
||||
|
||||
/// Rumble the weak motor at maximum intensity
|
||||
pub const WEAK_MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 0.0,
|
||||
weak_motor: 1.0,
|
||||
};
|
||||
|
||||
/// Rumble the strong motor at maximum intensity
|
||||
pub const STRONG_MAX: Self = GamepadRumbleIntensity {
|
||||
strong_motor: 1.0,
|
||||
weak_motor: 0.0,
|
||||
};
|
||||
|
||||
/// Creates a new rumble intensity with weak motor intensity set to the given value
|
||||
///
|
||||
/// Clamped within the 0 to 1 range
|
||||
pub const fn weak_motor(intensity: f32) -> Self {
|
||||
Self {
|
||||
weak_motor: intensity,
|
||||
strong_motor: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new rumble intensity with strong motor intensity set to the given value
|
||||
///
|
||||
/// Clamped within the 0 to 1 range
|
||||
pub const fn strong_motor(intensity: f32) -> Self {
|
||||
Self {
|
||||
strong_motor: intensity,
|
||||
weak_motor: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event that controls force-feedback rumbling of a [`Gamepad`]
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// Does nothing if the gamepad or platform does not support rumble.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_input::gamepad::{Gamepad, Gamepads, GamepadRumbleRequest, GamepadRumbleIntensity};
|
||||
/// # use bevy_ecs::prelude::{EventWriter, Res};
|
||||
/// # use bevy_utils::Duration;
|
||||
/// fn rumble_gamepad_system(
|
||||
/// mut rumble_requests: EventWriter<GamepadRumbleRequest>,
|
||||
/// gamepads: Res<Gamepads>
|
||||
/// ) {
|
||||
/// for gamepad in gamepads.iter() {
|
||||
/// rumble_requests.send(GamepadRumbleRequest::Add {
|
||||
/// gamepad,
|
||||
/// intensity: GamepadRumbleIntensity::MAX,
|
||||
/// duration: Duration::from_secs_f32(0.5),
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[doc(alias = "haptic feedback")]
|
||||
#[doc(alias = "force feedback")]
|
||||
#[doc(alias = "vibration")]
|
||||
#[doc(alias = "vibrate")]
|
||||
#[derive(Clone)]
|
||||
pub enum GamepadRumbleRequest {
|
||||
/// Add a rumble to the given gamepad.
|
||||
///
|
||||
/// Simultaneous rumble effects add up to the sum of their strengths.
|
||||
///
|
||||
/// Consequently, if two rumbles at half intensity are added at the same
|
||||
/// time, their intensities will be added up, and the controller will rumble
|
||||
/// at full intensity until one of the rumbles finishes, then the rumble
|
||||
/// will continue at the intensity of the remaining event.
|
||||
///
|
||||
/// To replace an existing rumble, send a [`GamepadRumbleRequest::Stop`] event first.
|
||||
Add {
|
||||
/// How long the gamepad should rumble
|
||||
duration: Duration,
|
||||
/// How intense the rumble should be
|
||||
intensity: GamepadRumbleIntensity,
|
||||
/// The gamepad to rumble
|
||||
gamepad: Gamepad,
|
||||
},
|
||||
/// Stop all running rumbles on the given [`Gamepad`]
|
||||
Stop { gamepad: Gamepad },
|
||||
}
|
||||
|
||||
impl GamepadRumbleRequest {
|
||||
/// Get the [`Gamepad`] associated with this request
|
||||
pub fn gamepad(&self) -> Gamepad {
|
||||
match self {
|
||||
Self::Add { gamepad, .. } | Self::Stop { gamepad } => *gamepad,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::gamepad::{AxisSettingsError, ButtonSettingsError};
|
||||
|
|
|
@ -39,8 +39,8 @@ use gamepad::{
|
|||
gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system,
|
||||
gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis,
|
||||
GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent,
|
||||
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings,
|
||||
Gamepads,
|
||||
GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
|
||||
GamepadRumbleRequest, GamepadSettings, Gamepads,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
|
@ -72,6 +72,7 @@ impl Plugin for InputPlugin {
|
|||
.add_event::<GamepadButtonChangedEvent>()
|
||||
.add_event::<GamepadAxisChangedEvent>()
|
||||
.add_event::<GamepadEvent>()
|
||||
.add_event::<GamepadRumbleRequest>()
|
||||
.init_resource::<GamepadSettings>()
|
||||
.init_resource::<Gamepads>()
|
||||
.init_resource::<Input<GamepadButton>>()
|
||||
|
|
|
@ -21,6 +21,7 @@ trace = [
|
|||
]
|
||||
trace_chrome = [ "bevy_log/tracing-chrome" ]
|
||||
trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy" ]
|
||||
trace_tracy_memory = ["bevy_log/trace_tracy_memory"]
|
||||
wgpu_trace = ["bevy_render/wgpu_trace"]
|
||||
debug_asset_server = ["bevy_asset/debug_asset_server"]
|
||||
detailed_trace = ["bevy_utils/detailed_trace"]
|
||||
|
@ -96,6 +97,8 @@ bevy_render = ["dep:bevy_render", "bevy_scene?/bevy_render"]
|
|||
# Enable assertions to check the validity of parameters passed to glam
|
||||
glam_assert = ["bevy_math/glam_assert"]
|
||||
|
||||
default_font = ["bevy_text?/default_font"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.11.0-dev" }
|
||||
|
|
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
|||
|
||||
[features]
|
||||
trace = [ "tracing-error" ]
|
||||
trace_tracy_memory = ["dep:tracy-client"]
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
|
||||
|
@ -21,6 +22,7 @@ tracing-chrome = { version = "0.7.0", optional = true }
|
|||
tracing-tracy = { version = "0.10.0", optional = true }
|
||||
tracing-log = "0.1.2"
|
||||
tracing-error = { version = "0.2.0", optional = true }
|
||||
tracy-client = { version = "0.15", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_log-sys = "0.2.0"
|
||||
|
|
|
@ -18,6 +18,11 @@ use std::panic;
|
|||
#[cfg(target_os = "android")]
|
||||
mod android_tracing;
|
||||
|
||||
#[cfg(feature = "trace_tracy_memory")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||
|
||||
pub mod prelude {
|
||||
//! The Bevy Log Prelude.
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -160,6 +160,8 @@ pub fn ensure_no_collision(value: Ident, haystack: TokenStream) -> Ident {
|
|||
/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait
|
||||
/// - `trait_path`: The path [`syn::Path`] to the label trait
|
||||
pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream {
|
||||
let bevy_utils_path = BevyManifest::default().get_path("bevy_utils");
|
||||
|
||||
let ident = input.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
|
||||
|
@ -178,6 +180,22 @@ pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> To
|
|||
fn dyn_clone(&self) -> std::boxed::Box<dyn #trait_path> {
|
||||
std::boxed::Box::new(std::clone::Clone::clone(self))
|
||||
}
|
||||
|
||||
fn as_dyn_eq(&self) -> &dyn #bevy_utils_path::label::DynEq {
|
||||
self
|
||||
}
|
||||
|
||||
fn dyn_hash(&self, mut state: &mut dyn ::std::hash::Hasher) {
|
||||
let ty_id = #trait_path::inner_type_id(self);
|
||||
::std::hash::Hash::hash(&ty_id, &mut state);
|
||||
::std::hash::Hash::hash(self, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics ::std::convert::AsRef<dyn #trait_path> for #ident #ty_generics #where_clause {
|
||||
fn as_ref(&self) -> &dyn #trait_path {
|
||||
self
|
||||
}
|
||||
}
|
||||
})
|
||||
.into()
|
||||
|
|
|
@ -2040,6 +2040,18 @@ pub fn check_light_mesh_visibility(
|
|||
frustum_visible_entities.entities.push(entity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
computed_visibility.set_visible_in_view();
|
||||
for view in frusta.frusta.keys() {
|
||||
let view_visible_entities = visible_entities
|
||||
.entities
|
||||
.get_mut(view)
|
||||
.expect("Per-view visible entities should have been inserted already");
|
||||
|
||||
for frustum_visible_entities in view_visible_entities {
|
||||
frustum_visible_entities.entities.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,10 @@ fn vertex(vertex: Vertex) -> VertexOutput {
|
|||
|
||||
#ifdef PREPASS_FRAGMENT
|
||||
struct FragmentInput {
|
||||
#ifdef VERTEX_UVS
|
||||
@location(0) uv: vec2<f32>,
|
||||
#endif // VERTEX_UVS
|
||||
|
||||
#ifdef NORMAL_PREPASS
|
||||
@location(1) world_normal: vec3<f32>,
|
||||
#endif // NORMAL_PREPASS
|
||||
|
|
|
@ -1231,8 +1231,8 @@ impl<P: PhaseItem> RenderCommand<P> for DrawMesh {
|
|||
pass.set_index_buffer(buffer.slice(..), 0, *index_format);
|
||||
pass.draw_indexed(0..*count, 0, 0..1);
|
||||
}
|
||||
GpuBufferInfo::NonIndexed { vertex_count } => {
|
||||
pass.draw(0..*vertex_count, 0..1);
|
||||
GpuBufferInfo::NonIndexed => {
|
||||
pass.draw(0..gpu_mesh.vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
RenderCommandResult::Success
|
||||
|
|
|
@ -23,8 +23,8 @@ struct FragmentInput {
|
|||
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
|
||||
let is_orthographic = view.projection[3].w == 1.0;
|
||||
let V = calculate_view(in.world_position, is_orthographic);
|
||||
var uv = in.uv;
|
||||
#ifdef VERTEX_UVS
|
||||
var uv = in.uv;
|
||||
#ifdef VERTEX_TANGENTS
|
||||
if ((material.flags & STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
|
||||
let N = in.world_normal;
|
||||
|
|
|
@ -148,7 +148,7 @@ impl<'a, A: IsAligned> Ptr<'a, A> {
|
|||
/// - If the `A` type parameter is [`Aligned`] then `inner` must be sufficiently aligned for the pointee type.
|
||||
/// - `inner` must have correct provenance to allow reads of the pointee type.
|
||||
/// - The lifetime `'a` must be constrained such that this [`Ptr`] will stay valid and nothing
|
||||
/// can mutate the pointee while this [`Ptr`] is live except through an `UnsafeCell`.
|
||||
/// can mutate the pointee while this [`Ptr`] is live except through an [`UnsafeCell`].
|
||||
#[inline]
|
||||
pub unsafe fn new(inner: NonNull<u8>) -> Self {
|
||||
Self(inner, PhantomData)
|
||||
|
@ -167,7 +167,7 @@ impl<'a, A: IsAligned> Ptr<'a, A> {
|
|||
///
|
||||
/// # Safety
|
||||
/// - `T` must be the erased pointee type for this [`Ptr`].
|
||||
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
|
||||
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
|
||||
/// for the pointee type `T`.
|
||||
#[inline]
|
||||
pub unsafe fn deref<T>(self) -> &'a T {
|
||||
|
@ -290,7 +290,7 @@ impl<'a, A: IsAligned> OwningPtr<'a, A> {
|
|||
///
|
||||
/// # Safety
|
||||
/// - `T` must be the erased pointee type for this [`OwningPtr`].
|
||||
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
|
||||
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
|
||||
/// for the pointee type `T`.
|
||||
#[inline]
|
||||
pub unsafe fn read<T>(self) -> T {
|
||||
|
@ -301,7 +301,7 @@ impl<'a, A: IsAligned> OwningPtr<'a, A> {
|
|||
///
|
||||
/// # Safety
|
||||
/// - `T` must be the erased pointee type for this [`OwningPtr`].
|
||||
/// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned
|
||||
/// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned
|
||||
/// for the pointee type `T`.
|
||||
#[inline]
|
||||
pub unsafe fn drop_as<T>(self) {
|
||||
|
|
|
@ -32,7 +32,7 @@ pub(crate) struct ResultSifter<T> {
|
|||
errors: Option<syn::Error>,
|
||||
}
|
||||
|
||||
/// Returns a `Member` made of `ident` or `index` if `ident` is None.
|
||||
/// Returns a [`Member`] made of `ident` or `index` if `ident` is None.
|
||||
///
|
||||
/// Rust struct syntax allows for `Struct { foo: "string" }` with explicitly
|
||||
/// named fields. It allows the `Struct { 0: "string" }` syntax when the struct
|
||||
|
|
|
@ -399,7 +399,10 @@ macro_rules! impl_reflect_for_hashmap {
|
|||
let mut dynamic_map = DynamicMap::default();
|
||||
dynamic_map.set_name(self.type_name().to_string());
|
||||
for (k, v) in self {
|
||||
dynamic_map.insert_boxed(k.clone_value(), v.clone_value());
|
||||
let key = K::from_reflect(k).unwrap_or_else(|| {
|
||||
panic!("Attempted to clone invalid key of type {}.", k.type_name())
|
||||
});
|
||||
dynamic_map.insert_boxed(Box::new(key), v.clone_value());
|
||||
}
|
||||
dynamic_map
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ impl TypeRegistry {
|
|||
///
|
||||
/// Most of the time [`TypeRegistry::register`] can be used instead to register a type you derived [`Reflect`] for.
|
||||
/// However, in cases where you want to add a piece of type data that was not included in the list of `#[reflect(...)]` type data in the derive,
|
||||
/// or where the type is generic and cannot register e.g. `ReflectSerialize` unconditionally without knowing the specific type parameters,
|
||||
/// or where the type is generic and cannot register e.g. [`ReflectSerialize`] unconditionally without knowing the specific type parameters,
|
||||
/// this method can be used to insert additional type data.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -220,7 +220,7 @@ impl TypeRegistry {
|
|||
.and_then(|id| self.registrations.get_mut(id))
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`TypeData`] of type `T` associated with the given `TypeId`.
|
||||
/// Returns a reference to the [`TypeData`] of type `T` associated with the given [`TypeId`].
|
||||
///
|
||||
/// The returned value may be used to downcast [`Reflect`] trait objects to
|
||||
/// trait objects of the trait used to generate `T`, provided that the
|
||||
|
@ -234,7 +234,7 @@ impl TypeRegistry {
|
|||
.and_then(|registration| registration.data::<T>())
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the [`TypeData`] of type `T` associated with the given `TypeId`.
|
||||
/// Returns a mutable reference to the [`TypeData`] of type `T` associated with the given [`TypeId`].
|
||||
///
|
||||
/// If the specified type has not been registered, or if `T` is not present
|
||||
/// in its type registration, returns `None`.
|
||||
|
@ -243,7 +243,7 @@ impl TypeRegistry {
|
|||
.and_then(|registration| registration.data_mut::<T>())
|
||||
}
|
||||
|
||||
/// Returns the [`TypeInfo`] associated with the given `TypeId`.
|
||||
/// Returns the [`TypeInfo`] associated with the given [`TypeId`].
|
||||
///
|
||||
/// If the specified type has not been registered, returns `None`.
|
||||
pub fn get_type_info(&self, type_id: TypeId) -> Option<&'static TypeInfo> {
|
||||
|
|
|
@ -2,7 +2,7 @@ error[E0599]: no method named `get_field` found for struct `Box<(dyn Reflect + '
|
|||
--> tests/reflect_derive/generics.fail.rs:14:9
|
||||
|
|
||||
14 | foo.get_field::<NoReflect>("a").unwrap();
|
||||
| ^^^^^^^^^ method not found in `Box<(dyn Reflect + 'static)>`
|
||||
| ^^^^^^^^^ method not found in `Box<dyn Reflect>`
|
||||
|
||||
error[E0277]: the trait bound `NoReflect: Reflect` is not satisfied
|
||||
--> tests/reflect_derive/generics.fail.rs:12:37
|
||||
|
|
|
@ -469,7 +469,7 @@ struct UniformBindingMeta {
|
|||
/// Represents the arguments for any general binding attribute.
|
||||
///
|
||||
/// If parsed, represents an attribute
|
||||
/// like `#[foo(LitInt, ...)]` where the rest is optional `NestedMeta`.
|
||||
/// like `#[foo(LitInt, ...)]` where the rest is optional [`NestedMeta`].
|
||||
enum BindingMeta {
|
||||
IndexOnly(LitInt),
|
||||
IndexWithOptions(BindingIndexOptions),
|
||||
|
|
|
@ -18,7 +18,7 @@ use bevy_ecs::{
|
|||
system::{Commands, Query, Res, ResMut, Resource},
|
||||
};
|
||||
use bevy_log::warn;
|
||||
use bevy_math::{Mat4, Ray, UVec2, UVec4, Vec2, Vec3};
|
||||
use bevy_math::{Mat4, Ray, Rect, UVec2, UVec4, Vec2, Vec3};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_reflect::FromReflect;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
|
@ -156,13 +156,16 @@ impl Camera {
|
|||
Some((min, max))
|
||||
}
|
||||
|
||||
/// The rendered logical bounds (minimum, maximum) of the camera. If the `viewport` field is set
|
||||
/// to [`Some`], this will be the rect of that custom viewport. Otherwise it will default to the
|
||||
/// The rendered logical bounds [`Rect`] of the camera. If the `viewport` field is set to
|
||||
/// [`Some`], this will be the rect of that custom viewport. Otherwise it will default to the
|
||||
/// full logical rect of the current [`RenderTarget`].
|
||||
#[inline]
|
||||
pub fn logical_viewport_rect(&self) -> Option<(Vec2, Vec2)> {
|
||||
pub fn logical_viewport_rect(&self) -> Option<Rect> {
|
||||
let (min, max) = self.physical_viewport_rect()?;
|
||||
Some((self.to_logical(min)?, self.to_logical(max)?))
|
||||
Some(Rect {
|
||||
min: self.to_logical(min)?,
|
||||
max: self.to_logical(max)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The logical size of this camera's viewport. If the `viewport` field is set to [`Some`], this
|
||||
|
@ -423,7 +426,7 @@ impl NormalizedRenderTarget {
|
|||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => windows
|
||||
.get(&window_ref.entity())
|
||||
.and_then(|window| window.swap_chain_texture.as_ref()),
|
||||
.and_then(|window| window.swap_chain_texture_view.as_ref()),
|
||||
NormalizedRenderTarget::Image(image_handle) => {
|
||||
images.get(image_handle).map(|image| &image.texture_view)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ impl Node for CameraDriverNode {
|
|||
continue;
|
||||
}
|
||||
|
||||
let Some(swap_chain_texture) = &window.swap_chain_texture else {
|
||||
let Some(swap_chain_texture) = &window.swap_chain_texture_view else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
|
|
@ -79,27 +79,27 @@ pub enum RenderSet {
|
|||
ExtractCommands,
|
||||
/// Prepare render resources from the extracted data for the GPU.
|
||||
Prepare,
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after `Prepare`.
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after [`Prepare`](RenderSet::Prepare).
|
||||
PrepareFlush,
|
||||
/// Create [`BindGroups`](crate::render_resource::BindGroup) that depend on
|
||||
/// Create [`BindGroups`](render_resource::BindGroup) that depend on
|
||||
/// [`Prepare`](RenderSet::Prepare) data and queue up draw calls to run during the
|
||||
/// [`Render`](RenderSet::Render) step.
|
||||
Queue,
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after `Queue`.
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after [`Queue`](RenderSet::Queue).
|
||||
QueueFlush,
|
||||
// TODO: This could probably be moved in favor of a system ordering abstraction in Render or Queue
|
||||
/// Sort the [`RenderPhases`](crate::render_phase::RenderPhase) here.
|
||||
/// Sort the [`RenderPhases`](render_phase::RenderPhase) here.
|
||||
PhaseSort,
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after `PhaseSort`.
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after [`PhaseSort`](RenderSet::PhaseSort).
|
||||
PhaseSortFlush,
|
||||
/// Actual rendering happens here.
|
||||
/// In most cases, only the render backend should insert resources here.
|
||||
Render,
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after `Render`.
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after [`Render`](RenderSet::Render).
|
||||
RenderFlush,
|
||||
/// Cleanup render resources here.
|
||||
Cleanup,
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after `Cleanup`.
|
||||
/// The copy of [`apply_system_buffers`] that runs immediately after [`Cleanup`](RenderSet::Cleanup).
|
||||
CleanupFlush,
|
||||
}
|
||||
|
||||
|
|
|
@ -260,7 +260,7 @@ impl Mesh {
|
|||
}
|
||||
|
||||
/// Computes and returns the vertex data of the mesh as bytes.
|
||||
/// Therefore the attributes are located in alphabetical order.
|
||||
/// Therefore the attributes are located in the order of their [`MeshVertexAttribute::id`].
|
||||
/// This is used to transform the vertex data into a GPU friendly format.
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -820,6 +820,7 @@ impl From<&Indices> for IndexFormat {
|
|||
pub struct GpuMesh {
|
||||
/// Contains all attribute data for each vertex.
|
||||
pub vertex_buffer: Buffer,
|
||||
pub vertex_count: u32,
|
||||
pub buffer_info: GpuBufferInfo,
|
||||
pub primitive_topology: PrimitiveTopology,
|
||||
pub layout: MeshVertexBufferLayout,
|
||||
|
@ -834,9 +835,7 @@ pub enum GpuBufferInfo {
|
|||
count: u32,
|
||||
index_format: IndexFormat,
|
||||
},
|
||||
NonIndexed {
|
||||
vertex_count: u32,
|
||||
},
|
||||
NonIndexed,
|
||||
}
|
||||
|
||||
impl RenderAsset for Mesh {
|
||||
|
@ -861,11 +860,8 @@ impl RenderAsset for Mesh {
|
|||
contents: &vertex_buffer_data,
|
||||
});
|
||||
|
||||
let buffer_info = mesh.get_index_buffer_bytes().map_or(
|
||||
GpuBufferInfo::NonIndexed {
|
||||
vertex_count: mesh.count_vertices() as u32,
|
||||
},
|
||||
|data| GpuBufferInfo::Indexed {
|
||||
let buffer_info = if let Some(data) = mesh.get_index_buffer_bytes() {
|
||||
GpuBufferInfo::Indexed {
|
||||
buffer: render_device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
usage: BufferUsages::INDEX,
|
||||
contents: data,
|
||||
|
@ -873,13 +869,16 @@ impl RenderAsset for Mesh {
|
|||
}),
|
||||
count: mesh.indices().unwrap().len() as u32,
|
||||
index_format: mesh.indices().unwrap().into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
GpuBufferInfo::NonIndexed
|
||||
};
|
||||
|
||||
let mesh_vertex_buffer_layout = mesh.get_mesh_vertex_buffer_layout();
|
||||
|
||||
Ok(GpuMesh {
|
||||
vertex_buffer,
|
||||
vertex_count: mesh.count_vertices() as u32,
|
||||
buffer_info,
|
||||
primitive_topology: mesh.primitive_topology(),
|
||||
layout: mesh_vertex_buffer_layout,
|
||||
|
|
|
@ -6,13 +6,16 @@ use super::NodeId;
|
|||
/// They are used to describe the ordering (which node has to run first)
|
||||
/// and may be of two kinds: [`NodeEdge`](Self::NodeEdge) and [`SlotEdge`](Self::SlotEdge).
|
||||
///
|
||||
/// Edges are added via the `render_graph::add_node_edge(output_node, input_node)` and the
|
||||
/// `render_graph::add_slot_edge(output_node, output_slot, input_node, input_slot)` methods.
|
||||
/// Edges are added via the [`RenderGraph::add_node_edge`] and the
|
||||
/// [`RenderGraph::add_slot_edge`] methods.
|
||||
///
|
||||
/// The former simply states that the `output_node` has to be run before the `input_node`,
|
||||
/// while the later connects an output slot of the `output_node`
|
||||
/// with an input slot of the `input_node` to pass additional data along.
|
||||
/// For more information see [`SlotType`](super::SlotType).
|
||||
///
|
||||
/// [`RenderGraph::add_node_edge`]: crate::render_graph::RenderGraph::add_node_edge
|
||||
/// [`RenderGraph::add_slot_edge`]: crate::render_graph::RenderGraph::add_slot_edge
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Edge {
|
||||
/// An edge describing to ordering of both nodes (`output_node` before `input_node`)
|
||||
|
|
|
@ -18,7 +18,7 @@ use std::{any::TypeId, fmt::Debug, hash::Hash};
|
|||
/// [`RenderCommand`]s. For more details and an example see the [`RenderCommand`] documentation.
|
||||
pub trait Draw<P: PhaseItem>: Send + Sync + 'static {
|
||||
/// Prepares the draw function to be used. This is called once and only once before the phase
|
||||
/// begins. There may be zero or more `draw` calls following a call to this function.
|
||||
/// begins. There may be zero or more [`draw`](Draw::draw) calls following a call to this function.
|
||||
/// Implementing this is optional.
|
||||
#[allow(unused_variables)]
|
||||
fn prepare(&mut self, world: &'_ World) {}
|
||||
|
@ -249,7 +249,7 @@ where
|
|||
C::Param: ReadOnlySystemParam,
|
||||
{
|
||||
/// Prepares the render command to be used. This is called once and only once before the phase
|
||||
/// begins. There may be zero or more `draw` calls following a call to this function.
|
||||
/// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function.
|
||||
fn prepare(&mut self, world: &'_ World) {
|
||||
self.state.update_archetypes(world);
|
||||
self.view.update_archetypes(world);
|
||||
|
|
|
@ -51,31 +51,21 @@ define_atomic_id!(TextureViewId);
|
|||
render_resource_wrapper!(ErasedTextureView, wgpu::TextureView);
|
||||
render_resource_wrapper!(ErasedSurfaceTexture, wgpu::SurfaceTexture);
|
||||
|
||||
/// This type combines wgpu's [`TextureView`](wgpu::TextureView) and
|
||||
/// [`SurfaceTexture`](wgpu::SurfaceTexture) into the same interface.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TextureViewValue {
|
||||
/// The value is an actual wgpu [`TextureView`](wgpu::TextureView).
|
||||
TextureView(ErasedTextureView),
|
||||
|
||||
/// The value is a wgpu [`SurfaceTexture`](wgpu::SurfaceTexture), but dereferences to
|
||||
/// a [`TextureView`](wgpu::TextureView).
|
||||
SurfaceTexture {
|
||||
// NOTE: The order of these fields is important because the view must be dropped before the
|
||||
// frame is dropped
|
||||
view: ErasedTextureView,
|
||||
texture: ErasedSurfaceTexture,
|
||||
},
|
||||
}
|
||||
|
||||
/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup).
|
||||
///
|
||||
/// May be converted from a [`TextureView`](wgpu::TextureView) or [`SurfaceTexture`](wgpu::SurfaceTexture)
|
||||
/// or dereferences to a wgpu [`TextureView`](wgpu::TextureView).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextureView {
|
||||
id: TextureViewId,
|
||||
value: TextureViewValue,
|
||||
value: ErasedTextureView,
|
||||
}
|
||||
|
||||
pub struct SurfaceTexture {
|
||||
value: ErasedSurfaceTexture,
|
||||
}
|
||||
|
||||
impl SurfaceTexture {
|
||||
pub fn try_unwrap(self) -> Option<wgpu::SurfaceTexture> {
|
||||
self.value.try_unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureView {
|
||||
|
@ -84,34 +74,21 @@ impl TextureView {
|
|||
pub fn id(&self) -> TextureViewId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns the [`SurfaceTexture`](wgpu::SurfaceTexture) of the texture view if it is of that type.
|
||||
#[inline]
|
||||
pub fn take_surface_texture(self) -> Option<wgpu::SurfaceTexture> {
|
||||
match self.value {
|
||||
TextureViewValue::TextureView(_) => None,
|
||||
TextureViewValue::SurfaceTexture { texture, .. } => texture.try_unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::TextureView> for TextureView {
|
||||
fn from(value: wgpu::TextureView) -> Self {
|
||||
TextureView {
|
||||
id: TextureViewId::new(),
|
||||
value: TextureViewValue::TextureView(ErasedTextureView::new(value)),
|
||||
value: ErasedTextureView::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wgpu::SurfaceTexture> for TextureView {
|
||||
impl From<wgpu::SurfaceTexture> for SurfaceTexture {
|
||||
fn from(value: wgpu::SurfaceTexture) -> Self {
|
||||
let view = ErasedTextureView::new(value.texture.create_view(&Default::default()));
|
||||
let texture = ErasedSurfaceTexture::new(value);
|
||||
|
||||
TextureView {
|
||||
id: TextureViewId::new(),
|
||||
value: TextureViewValue::SurfaceTexture { texture, view },
|
||||
SurfaceTexture {
|
||||
value: ErasedSurfaceTexture::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,10 +98,16 @@ impl Deref for TextureView {
|
|||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self.value {
|
||||
TextureViewValue::TextureView(value) => value,
|
||||
TextureViewValue::SurfaceTexture { view, .. } => view,
|
||||
}
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SurfaceTexture {
|
||||
type Target = wgpu::SurfaceTexture;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,9 +57,12 @@ impl RenderGraphRunner {
|
|||
render_device: RenderDevice,
|
||||
queue: &wgpu::Queue,
|
||||
world: &World,
|
||||
finalizer: impl FnOnce(&mut wgpu::CommandEncoder),
|
||||
) -> Result<(), RenderGraphRunnerError> {
|
||||
let mut render_context = RenderContext::new(render_device);
|
||||
Self::run_graph(graph, None, &mut render_context, world, &[], None)?;
|
||||
finalizer(render_context.command_encoder());
|
||||
|
||||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _span = info_span!("submit_graph_commands").entered();
|
||||
|
|
|
@ -35,6 +35,9 @@ pub fn render_system(world: &mut World) {
|
|||
render_device.clone(), // TODO: is this clone really necessary?
|
||||
&render_queue.0,
|
||||
world,
|
||||
|encoder| {
|
||||
crate::view::screenshot::submit_screenshot_commands(world, encoder);
|
||||
},
|
||||
) {
|
||||
error!("Error running render graph:");
|
||||
{
|
||||
|
@ -66,8 +69,8 @@ pub fn render_system(world: &mut World) {
|
|||
|
||||
let mut windows = world.resource_mut::<ExtractedWindows>();
|
||||
for window in windows.values_mut() {
|
||||
if let Some(texture_view) = window.swap_chain_texture.take() {
|
||||
if let Some(surface_texture) = texture_view.take_surface_texture() {
|
||||
if let Some(wrapped_texture) = window.swap_chain_texture.take() {
|
||||
if let Some(surface_texture) = wrapped_texture.try_unwrap() {
|
||||
surface_texture.present();
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +84,8 @@ pub fn render_system(world: &mut World) {
|
|||
);
|
||||
}
|
||||
|
||||
crate::view::screenshot::collect_screenshots(world);
|
||||
|
||||
// update the time and send it to the app world
|
||||
let time_sender = world.resource::<TimeSender>();
|
||||
time_sender.0.try_send(Instant::now()).expect(
|
||||
|
@ -102,7 +107,7 @@ pub struct RenderAdapter(pub Arc<Adapter>);
|
|||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct RenderInstance(pub Instance);
|
||||
|
||||
/// The `AdapterInfo` of the adapter in use by the renderer.
|
||||
/// The [`AdapterInfo`] of the adapter in use by the renderer.
|
||||
#[derive(Resource, Clone, Deref, DerefMut)]
|
||||
pub struct RenderAdapterInfo(pub AdapterInfo);
|
||||
|
||||
|
|
|
@ -128,19 +128,19 @@ pub enum ImageSampler {
|
|||
}
|
||||
|
||||
impl ImageSampler {
|
||||
/// Returns an image sampler with `Linear` min and mag filters
|
||||
/// Returns an image sampler with [`Linear`](crate::render_resource::FilterMode::Linear) min and mag filters
|
||||
#[inline]
|
||||
pub fn linear() -> ImageSampler {
|
||||
ImageSampler::Descriptor(Self::linear_descriptor())
|
||||
}
|
||||
|
||||
/// Returns an image sampler with `nearest` min and mag filters
|
||||
/// Returns an image sampler with [`Nearest`](crate::render_resource::FilterMode::Nearest) min and mag filters
|
||||
#[inline]
|
||||
pub fn nearest() -> ImageSampler {
|
||||
ImageSampler::Descriptor(Self::nearest_descriptor())
|
||||
}
|
||||
|
||||
/// Returns a sampler descriptor with `Linear` min and mag filters
|
||||
/// Returns a sampler descriptor with [`Linear`](crate::render_resource::FilterMode::Linear) min and mag filters
|
||||
#[inline]
|
||||
pub fn linear_descriptor() -> wgpu::SamplerDescriptor<'static> {
|
||||
wgpu::SamplerDescriptor {
|
||||
|
@ -151,7 +151,7 @@ impl ImageSampler {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a sampler descriptor with `Nearest` min and mag filters
|
||||
/// Returns a sampler descriptor with [`Nearest`](crate::render_resource::FilterMode::Nearest) min and mag filters
|
||||
#[inline]
|
||||
pub fn nearest_descriptor() -> wgpu::SamplerDescriptor<'static> {
|
||||
wgpu::SamplerDescriptor {
|
||||
|
@ -230,7 +230,6 @@ impl Image {
|
|||
///
|
||||
/// # Panics
|
||||
/// Panics if the size of the `format` is not a multiple of the length of the `pixel` data.
|
||||
/// do not match.
|
||||
pub fn new_fill(
|
||||
size: Extent3d,
|
||||
dimension: TextureDimension,
|
||||
|
|
|
@ -174,6 +174,7 @@ impl Image {
|
|||
/// - `TextureFormat::R8Unorm`
|
||||
/// - `TextureFormat::Rg8Unorm`
|
||||
/// - `TextureFormat::Rgba8UnormSrgb`
|
||||
/// - `TextureFormat::Bgra8UnormSrgb`
|
||||
///
|
||||
/// To convert [`Image`] to a different format see: [`Image::convert`].
|
||||
pub fn try_into_dynamic(self) -> anyhow::Result<DynamicImage> {
|
||||
|
@ -196,6 +197,20 @@ impl Image {
|
|||
self.data,
|
||||
)
|
||||
.map(DynamicImage::ImageRgba8),
|
||||
// This format is commonly used as the format for the swapchain texture
|
||||
// This conversion is added here to support screenshots
|
||||
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
|
||||
self.texture_descriptor.size.width,
|
||||
self.texture_descriptor.size.height,
|
||||
{
|
||||
let mut data = self.data;
|
||||
for bgra in data.chunks_exact_mut(4) {
|
||||
bgra.swap(0, 2);
|
||||
}
|
||||
data
|
||||
},
|
||||
)
|
||||
.map(DynamicImage::ImageRgba8),
|
||||
// Throw and error if conversion isn't supported
|
||||
texture_format => {
|
||||
return Err(anyhow!(
|
||||
|
|
|
@ -25,7 +25,7 @@ use crate::{
|
|||
/// User indication of whether an entity is visible. Propagates down the entity hierarchy.
|
||||
///
|
||||
/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who
|
||||
/// are set to `Inherited` will also be hidden.
|
||||
/// are set to [`Inherited`](Self::Inherited) will also be hidden.
|
||||
///
|
||||
/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and
|
||||
/// `Visibility` to set the values of each entity's [`ComputedVisibility`] component.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
render_resource::TextureView,
|
||||
render_resource::{PipelineCache, SpecializedRenderPipelines, SurfaceTexture, TextureView},
|
||||
renderer::{RenderAdapter, RenderDevice, RenderInstance},
|
||||
texture::TextureFormatPixelInfo,
|
||||
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
|
@ -10,7 +11,13 @@ use bevy_window::{
|
|||
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use wgpu::TextureFormat;
|
||||
use wgpu::{BufferUsages, TextureFormat, TextureUsages};
|
||||
|
||||
pub mod screenshot;
|
||||
|
||||
use screenshot::{
|
||||
ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
|
||||
};
|
||||
|
||||
use super::Msaa;
|
||||
|
||||
|
@ -27,10 +34,13 @@ pub enum WindowSystem {
|
|||
|
||||
impl Plugin for WindowRenderPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugin(ScreenshotPlugin);
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app
|
||||
.init_resource::<ExtractedWindows>()
|
||||
.init_resource::<WindowSurfaces>()
|
||||
.init_resource::<ScreenshotToScreenPipeline>()
|
||||
.init_non_send_resource::<NonSendMarker>()
|
||||
.add_systems(ExtractSchedule, extract_windows)
|
||||
.configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare))
|
||||
|
@ -46,11 +56,26 @@ pub struct ExtractedWindow {
|
|||
pub physical_width: u32,
|
||||
pub physical_height: u32,
|
||||
pub present_mode: PresentMode,
|
||||
pub swap_chain_texture: Option<TextureView>,
|
||||
/// Note: this will not always be the swap chain texture view. When taking a screenshot,
|
||||
/// this will point to an alternative texture instead to allow for copying the render result
|
||||
/// to CPU memory.
|
||||
pub swap_chain_texture_view: Option<TextureView>,
|
||||
pub swap_chain_texture: Option<SurfaceTexture>,
|
||||
pub swap_chain_texture_format: Option<TextureFormat>,
|
||||
pub screenshot_memory: Option<ScreenshotPreparedState>,
|
||||
pub size_changed: bool,
|
||||
pub present_mode_changed: bool,
|
||||
pub alpha_mode: CompositeAlphaMode,
|
||||
pub screenshot_func: Option<screenshot::ScreenshotFn>,
|
||||
}
|
||||
|
||||
impl ExtractedWindow {
|
||||
fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
|
||||
self.swap_chain_texture_view = Some(TextureView::from(
|
||||
frame.texture.create_view(&Default::default()),
|
||||
));
|
||||
self.swap_chain_texture = Some(SurfaceTexture::from(frame));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
|
@ -75,6 +100,7 @@ impl DerefMut for ExtractedWindows {
|
|||
|
||||
fn extract_windows(
|
||||
mut extracted_windows: ResMut<ExtractedWindows>,
|
||||
screenshot_manager: Extract<Res<ScreenshotManager>>,
|
||||
mut closed: Extract<EventReader<WindowClosed>>,
|
||||
windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
|
||||
) {
|
||||
|
@ -95,14 +121,17 @@ fn extract_windows(
|
|||
physical_height: new_height,
|
||||
present_mode: window.present_mode,
|
||||
swap_chain_texture: None,
|
||||
swap_chain_texture_view: None,
|
||||
size_changed: false,
|
||||
swap_chain_texture_format: None,
|
||||
present_mode_changed: false,
|
||||
alpha_mode: window.composite_alpha_mode,
|
||||
screenshot_func: None,
|
||||
screenshot_memory: None,
|
||||
});
|
||||
|
||||
// NOTE: Drop the swap chain frame here
|
||||
extracted_window.swap_chain_texture = None;
|
||||
extracted_window.swap_chain_texture_view = None;
|
||||
extracted_window.size_changed = new_width != extracted_window.physical_width
|
||||
|| new_height != extracted_window.physical_height;
|
||||
extracted_window.present_mode_changed =
|
||||
|
@ -132,6 +161,15 @@ fn extract_windows(
|
|||
for closed_window in closed.iter() {
|
||||
extracted_windows.remove(&closed_window.window);
|
||||
}
|
||||
// This lock will never block because `callbacks` is `pub(crate)` and this is the singular callsite where it's locked.
|
||||
// Even if a user had multiple copies of this system, since the system has a mutable resource access the two systems would never run
|
||||
// at the same time
|
||||
// TODO: since this is guaranteed, should the lock be replaced with an UnsafeCell to remove the overhead, or is it minor enough to be ignored?
|
||||
for (window, screenshot_func) in screenshot_manager.callbacks.lock().drain() {
|
||||
if let Some(window) = extracted_windows.get_mut(&window) {
|
||||
window.screenshot_func = Some(screenshot_func);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SurfaceData {
|
||||
|
@ -167,6 +205,7 @@ pub struct WindowSurfaces {
|
|||
/// another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and
|
||||
/// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or
|
||||
/// later.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn prepare_windows(
|
||||
// By accessing a NonSend resource, we tell the scheduler to put this system on the main thread,
|
||||
// which is necessary for some OS s
|
||||
|
@ -176,6 +215,9 @@ pub fn prepare_windows(
|
|||
render_device: Res<RenderDevice>,
|
||||
render_instance: Res<RenderInstance>,
|
||||
render_adapter: Res<RenderAdapter>,
|
||||
screenshot_pipeline: Res<ScreenshotToScreenPipeline>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
mut pipelines: ResMut<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>,
|
||||
mut msaa: ResMut<Msaa>,
|
||||
) {
|
||||
for window in windows.windows.values_mut() {
|
||||
|
@ -285,18 +327,18 @@ pub fn prepare_windows(
|
|||
let frame = surface
|
||||
.get_current_texture()
|
||||
.expect("Error configuring surface");
|
||||
window.swap_chain_texture = Some(TextureView::from(frame));
|
||||
window.set_swapchain_texture(frame);
|
||||
} else {
|
||||
match surface.get_current_texture() {
|
||||
Ok(frame) => {
|
||||
window.swap_chain_texture = Some(TextureView::from(frame));
|
||||
window.set_swapchain_texture(frame);
|
||||
}
|
||||
Err(wgpu::SurfaceError::Outdated) => {
|
||||
render_device.configure_surface(surface, &surface_configuration);
|
||||
let frame = surface
|
||||
.get_current_texture()
|
||||
.expect("Error reconfiguring surface");
|
||||
window.swap_chain_texture = Some(TextureView::from(frame));
|
||||
window.set_swapchain_texture(frame);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => {
|
||||
|
@ -311,5 +353,55 @@ pub fn prepare_windows(
|
|||
}
|
||||
};
|
||||
window.swap_chain_texture_format = Some(surface_data.format);
|
||||
|
||||
if window.screenshot_func.is_some() {
|
||||
let texture = render_device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("screenshot-capture-rendertarget"),
|
||||
size: wgpu::Extent3d {
|
||||
width: surface_configuration.width,
|
||||
height: surface_configuration.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: surface_configuration.format,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT
|
||||
| TextureUsages::COPY_SRC
|
||||
| TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let texture_view = texture.create_view(&Default::default());
|
||||
let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("screenshot-transfer-buffer"),
|
||||
size: screenshot::get_aligned_size(
|
||||
window.physical_width,
|
||||
window.physical_height,
|
||||
surface_data.format.pixel_size() as u32,
|
||||
) as u64,
|
||||
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bind_group = render_device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("screenshot-to-screen-bind-group"),
|
||||
layout: &screenshot_pipeline.bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||||
}],
|
||||
});
|
||||
let pipeline_id = pipelines.specialize(
|
||||
&pipeline_cache,
|
||||
&screenshot_pipeline,
|
||||
surface_configuration.format,
|
||||
);
|
||||
window.swap_chain_texture_view = Some(texture_view);
|
||||
window.screenshot_memory = Some(ScreenshotPreparedState {
|
||||
texture,
|
||||
buffer,
|
||||
bind_group,
|
||||
pipeline_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
315
crates/bevy_render/src/view/window/screenshot.rs
Normal file
315
crates/bevy_render/src/view/window/screenshot.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
use std::{borrow::Cow, num::NonZeroU32, path::Path};
|
||||
|
||||
use bevy_app::Plugin;
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_log::{error, info, info_span};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_tasks::AsyncComputeTaskPool;
|
||||
use bevy_utils::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use thiserror::Error;
|
||||
use wgpu::{
|
||||
CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
prelude::{Image, Shader},
|
||||
render_resource::{
|
||||
BindGroup, BindGroupLayout, Buffer, CachedRenderPipelineId, FragmentState, PipelineCache,
|
||||
RenderPipelineDescriptor, SpecializedRenderPipeline, SpecializedRenderPipelines, Texture,
|
||||
VertexState,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
texture::TextureFormatPixelInfo,
|
||||
RenderApp,
|
||||
};
|
||||
|
||||
use super::ExtractedWindows;
|
||||
|
||||
pub type ScreenshotFn = Box<dyn FnOnce(Image) + Send + Sync>;
|
||||
|
||||
/// A resource which allows for taking screenshots of the window.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct ScreenshotManager {
|
||||
// this is in a mutex to enable extraction with only an immutable reference
|
||||
pub(crate) callbacks: Mutex<HashMap<Entity, ScreenshotFn>>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("A screenshot for this window has already been requested.")]
|
||||
pub struct ScreenshotAlreadyRequestedError;
|
||||
|
||||
impl ScreenshotManager {
|
||||
/// Signals the renderer to take a screenshot of this frame.
|
||||
///
|
||||
/// The given callback will eventually be called on one of the [`AsyncComputeTaskPool`]s threads.
|
||||
pub fn take_screenshot(
|
||||
&mut self,
|
||||
window: Entity,
|
||||
callback: impl FnOnce(Image) + Send + Sync + 'static,
|
||||
) -> Result<(), ScreenshotAlreadyRequestedError> {
|
||||
self.callbacks
|
||||
.get_mut()
|
||||
.try_insert(window, Box::new(callback))
|
||||
.map(|_| ())
|
||||
.map_err(|_| ScreenshotAlreadyRequestedError)
|
||||
}
|
||||
|
||||
/// Signals the renderer to take a screenshot of this frame.
|
||||
///
|
||||
/// The screenshot will eventually be saved to the given path, and the format will be derived from the extension.
|
||||
pub fn save_screenshot_to_disk(
|
||||
&mut self,
|
||||
window: Entity,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), ScreenshotAlreadyRequestedError> {
|
||||
let path = path.as_ref().to_owned();
|
||||
self.take_screenshot(window, move |img| match img.try_into_dynamic() {
|
||||
Ok(dyn_img) => match image::ImageFormat::from_path(&path) {
|
||||
Ok(format) => {
|
||||
// discard the alpha channel which stores brightness values when HDR is enabled to make sure
|
||||
// the screenshot looks right
|
||||
let img = dyn_img.to_rgb8();
|
||||
match img.save_with_format(&path, format) {
|
||||
Ok(_) => info!("Screenshot saved to {}", path.display()),
|
||||
Err(e) => error!("Cannot save screenshot, IO error: {e}"),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"),
|
||||
},
|
||||
Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScreenshotPlugin;
|
||||
|
||||
const SCREENSHOT_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11918575842344596158);
|
||||
|
||||
impl Plugin for ScreenshotPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.init_resource::<ScreenshotManager>();
|
||||
|
||||
load_internal_asset!(
|
||||
app,
|
||||
SCREENSHOT_SHADER_HANDLE,
|
||||
"screenshot.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn align_byte_size(value: u32) -> u32 {
|
||||
value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
|
||||
}
|
||||
|
||||
pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 {
|
||||
height * align_byte_size(width * pixel_size)
|
||||
}
|
||||
|
||||
pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout {
|
||||
ImageDataLayout {
|
||||
bytes_per_row: if height > 1 {
|
||||
// 1 = 1 row
|
||||
NonZeroU32::new(get_aligned_size(width, 1, format.pixel_size() as u32))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
rows_per_image: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct ScreenshotToScreenPipeline {
|
||||
pub bind_group_layout: BindGroupLayout,
|
||||
}
|
||||
|
||||
impl FromWorld for ScreenshotToScreenPipeline {
|
||||
fn from_world(render_world: &mut World) -> Self {
|
||||
let device = render_world.resource::<RenderDevice>();
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("screenshot-to-screen-bgl"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
Self { bind_group_layout }
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
|
||||
type Key = TextureFormat;
|
||||
|
||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||
RenderPipelineDescriptor {
|
||||
label: Some(Cow::Borrowed("screenshot-to-screen")),
|
||||
layout: vec![self.bind_group_layout.clone()],
|
||||
vertex: VertexState {
|
||||
buffers: vec![],
|
||||
shader_defs: vec![],
|
||||
entry_point: Cow::Borrowed("vs_main"),
|
||||
shader: SCREENSHOT_SHADER_HANDLE.typed(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
unclipped_depth: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: SCREENSHOT_SHADER_HANDLE.typed(),
|
||||
entry_point: Cow::Borrowed("fs_main"),
|
||||
shader_defs: vec![],
|
||||
targets: vec![Some(wgpu::ColorTargetState {
|
||||
format: key,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
push_constant_ranges: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScreenshotPreparedState {
|
||||
pub texture: Texture,
|
||||
pub buffer: Buffer,
|
||||
pub bind_group: BindGroup,
|
||||
pub pipeline_id: CachedRenderPipelineId,
|
||||
}
|
||||
|
||||
pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) {
|
||||
let windows = world.resource::<ExtractedWindows>();
|
||||
let pipelines = world.resource::<PipelineCache>();
|
||||
|
||||
for window in windows.values() {
|
||||
if let Some(memory) = &window.screenshot_memory {
|
||||
let width = window.physical_width;
|
||||
let height = window.physical_height;
|
||||
let texture_format = window.swap_chain_texture_format.unwrap();
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
memory.texture.as_image_copy(),
|
||||
wgpu::ImageCopyBuffer {
|
||||
buffer: &memory.buffer,
|
||||
layout: crate::view::screenshot::layout_data(width, height, texture_format),
|
||||
},
|
||||
Extent3d {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if let Some(pipeline) = pipelines.get_render_pipeline(memory.pipeline_id) {
|
||||
let true_swapchain_texture_view = window
|
||||
.swap_chain_texture
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.texture
|
||||
.create_view(&Default::default());
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("screenshot_to_screen_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &true_swapchain_texture_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, &memory.bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_screenshots(world: &mut World) {
|
||||
let _span = info_span!("collect_screenshots");
|
||||
|
||||
let mut windows = world.resource_mut::<ExtractedWindows>();
|
||||
for window in windows.values_mut() {
|
||||
if let Some(screenshot_func) = window.screenshot_func.take() {
|
||||
let width = window.physical_width;
|
||||
let height = window.physical_height;
|
||||
let texture_format = window.swap_chain_texture_format.unwrap();
|
||||
let pixel_size = texture_format.pixel_size();
|
||||
let ScreenshotPreparedState { buffer, .. } = window.screenshot_memory.take().unwrap();
|
||||
|
||||
let finish = async move {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
let buffer_slice = buffer.slice(..);
|
||||
// The polling for this map call is done every frame when the command queue is submitted.
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
let err = result.err();
|
||||
if err.is_some() {
|
||||
panic!("{}", err.unwrap().to_string());
|
||||
}
|
||||
tx.try_send(()).unwrap();
|
||||
});
|
||||
rx.recv().await.unwrap();
|
||||
let data = buffer_slice.get_mapped_range();
|
||||
// we immediately move the data to CPU memory to avoid holding the mapped view for long
|
||||
let mut result = Vec::from(&*data);
|
||||
drop(data);
|
||||
drop(buffer);
|
||||
|
||||
if result.len() != ((width * height) as usize * pixel_size) {
|
||||
// Our buffer has been padded because we needed to align to a multiple of 256.
|
||||
// We remove this padding here
|
||||
let initial_row_bytes = width as usize * pixel_size;
|
||||
let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize;
|
||||
|
||||
let mut take_offset = buffered_row_bytes;
|
||||
let mut place_offset = initial_row_bytes;
|
||||
for _ in 1..height {
|
||||
result.copy_within(
|
||||
take_offset..take_offset + buffered_row_bytes,
|
||||
place_offset,
|
||||
);
|
||||
take_offset += buffered_row_bytes;
|
||||
place_offset += initial_row_bytes;
|
||||
}
|
||||
result.truncate(initial_row_bytes * height as usize);
|
||||
}
|
||||
|
||||
screenshot_func(Image::new(
|
||||
Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
wgpu::TextureDimension::D2,
|
||||
result,
|
||||
texture_format,
|
||||
));
|
||||
};
|
||||
|
||||
AsyncComputeTaskPool::get().spawn(finish).detach();
|
||||
}
|
||||
}
|
||||
}
|
16
crates/bevy_render/src/view/window/screenshot.wgsl
Normal file
16
crates/bevy_render/src/view/window/screenshot.wgsl
Normal file
|
@ -0,0 +1,16 @@
|
|||
// This vertex shader will create a triangle that will cover the entire screen
|
||||
// with minimal effort, avoiding the need for a vertex buffer etc.
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
|
||||
let x = f32((in_vertex_index & 1u) << 2u);
|
||||
let y = f32((in_vertex_index & 2u) << 1u);
|
||||
return vec4<f32>(x - 1.0, y - 1.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var t: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let coords = floor(pos.xy);
|
||||
return textureLoad(t, vec2<i32>(coords), 0i);
|
||||
}
|
|
@ -36,10 +36,10 @@ pub struct DynamicScene {
|
|||
|
||||
/// A reflection-powered serializable representation of an entity and its components.
|
||||
pub struct DynamicEntity {
|
||||
/// The transiently unique identifier of a corresponding `Entity`.
|
||||
/// The transiently unique identifier of a corresponding [`Entity`](bevy_ecs::entity::Entity).
|
||||
pub entity: u32,
|
||||
/// A vector of boxed components that belong to the given entity and
|
||||
/// implement the `Reflect` trait.
|
||||
/// implement the [`Reflect`] trait.
|
||||
pub components: Vec<Box<dyn Reflect>>,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ pub enum Collision {
|
|||
// TODO: ideally we can remove this once bevy gets a physics system
|
||||
/// Axis-aligned bounding box collision with "side" detection
|
||||
/// * `a_pos` and `b_pos` are the center positions of the rectangles, typically obtained by
|
||||
/// extracting the `translation` field from a `Transform` component
|
||||
/// extracting the `translation` field from a [`Transform`](bevy_transform::components::Transform) component
|
||||
/// * `a_size` and `b_size` are the dimensions (width and height) of the rectangles.
|
||||
///
|
||||
/// The return value is the side of `B` that `A` has collided with. `Left` means that
|
||||
/// `A` collided with `B`'s left side. `Top` means that `A` collided with `B`'s top side.
|
||||
/// If the collision occurs on multiple sides, the side with the deepest penetration is returned.
|
||||
/// If all sides are involved, `Inside` is returned.
|
||||
/// The return value is the side of `B` that `A` has collided with. [`Collision::Left`] means that
|
||||
/// `A` collided with `B`'s left side. [`Collision::Top`] means that `A` collided with `B`'s top side.
|
||||
/// If the collision occurs on multiple sides, the side with the shallowest penetration is returned.
|
||||
/// If all sides are involved, [`Collision::Inside`] is returned.
|
||||
pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option<Collision> {
|
||||
let a_min = a_pos.truncate() - a_size / 2.0;
|
||||
let a_max = a_pos.truncate() + a_size / 2.0;
|
||||
|
|
|
@ -29,13 +29,17 @@ pub use texture_atlas::*;
|
|||
pub use texture_atlas_builder::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Assets, HandleUntyped};
|
||||
use bevy_asset::{AddAsset, Assets, Handle, HandleUntyped};
|
||||
use bevy_core_pipeline::core_2d::Transparent2d;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
mesh::Mesh,
|
||||
primitives::Aabb,
|
||||
render_phase::AddRenderCommand,
|
||||
render_resource::{Shader, SpecializedRenderPipelines},
|
||||
texture::Image,
|
||||
view::{NoFrustumCulling, VisibilitySystems},
|
||||
ExtractSchedule, Render, RenderApp, RenderSet,
|
||||
};
|
||||
|
||||
|
@ -58,10 +62,15 @@ impl Plugin for SpritePlugin {
|
|||
app.add_asset::<TextureAtlas>()
|
||||
.register_asset_reflect::<TextureAtlas>()
|
||||
.register_type::<Sprite>()
|
||||
.register_type::<TextureAtlasSprite>()
|
||||
.register_type::<Anchor>()
|
||||
.register_type::<Mesh2dHandle>()
|
||||
.add_plugin(Mesh2dRenderPlugin)
|
||||
.add_plugin(ColorMaterialPlugin);
|
||||
.add_plugin(ColorMaterialPlugin)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
|
||||
);
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app
|
||||
|
@ -88,3 +97,53 @@ impl Plugin for SpritePlugin {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_bounds_2d(
|
||||
mut commands: Commands,
|
||||
meshes: Res<Assets<Mesh>>,
|
||||
images: Res<Assets<Image>>,
|
||||
atlases: Res<Assets<TextureAtlas>>,
|
||||
meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
||||
sprites_without_aabb: Query<
|
||||
(Entity, &Sprite, &Handle<Image>),
|
||||
(Without<Aabb>, Without<NoFrustumCulling>),
|
||||
>,
|
||||
atlases_without_aabb: Query<
|
||||
(Entity, &TextureAtlasSprite, &Handle<TextureAtlas>),
|
||||
(Without<Aabb>, Without<NoFrustumCulling>),
|
||||
>,
|
||||
) {
|
||||
for (entity, mesh_handle) in &meshes_without_aabb {
|
||||
if let Some(mesh) = meshes.get(&mesh_handle.0) {
|
||||
if let Some(aabb) = mesh.compute_aabb() {
|
||||
commands.entity(entity).insert(aabb);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (entity, sprite, texture_handle) in &sprites_without_aabb {
|
||||
if let Some(size) = sprite
|
||||
.custom_size
|
||||
.or_else(|| images.get(texture_handle).map(|image| image.size()))
|
||||
{
|
||||
let aabb = Aabb {
|
||||
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
|
||||
half_extents: (0.5 * size).extend(0.0).into(),
|
||||
};
|
||||
commands.entity(entity).insert(aabb);
|
||||
}
|
||||
}
|
||||
for (entity, atlas_sprite, atlas_handle) in &atlases_without_aabb {
|
||||
if let Some(size) = atlas_sprite.custom_size.or_else(|| {
|
||||
atlases
|
||||
.get(atlas_handle)
|
||||
.and_then(|atlas| atlas.textures.get(atlas_sprite.index))
|
||||
.map(|rect| (rect.min - rect.max).abs())
|
||||
}) {
|
||||
let aabb = Aabb {
|
||||
center: (-atlas_sprite.anchor.as_vec() * size).extend(0.0).into(),
|
||||
half_extents: (0.5 * size).extend(0.0).into(),
|
||||
};
|
||||
commands.entity(entity).insert(aabb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -599,8 +599,8 @@ impl<P: PhaseItem> RenderCommand<P> for DrawMesh2d {
|
|||
pass.set_index_buffer(buffer.slice(..), 0, *index_format);
|
||||
pass.draw_indexed(0..*count, 0, 0..1);
|
||||
}
|
||||
GpuBufferInfo::NonIndexed { vertex_count } => {
|
||||
pass.draw(0..*vertex_count, 0..1);
|
||||
GpuBufferInfo::NonIndexed => {
|
||||
pass.draw(0..gpu_mesh.vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
RenderCommandResult::Success
|
||||
|
|
|
@ -309,7 +309,7 @@ pub struct ExtractedSprite {
|
|||
pub rect: Option<Rect>,
|
||||
/// Change the on-screen size of the sprite
|
||||
pub custom_size: Option<Vec2>,
|
||||
/// Handle to the `Image` of this sprite
|
||||
/// Handle to the [`Image`] of this sprite
|
||||
/// PERF: storing a `HandleId` instead of `Handle<Image>` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped)
|
||||
pub image_handle_id: HandleId,
|
||||
pub flip_x: bool,
|
||||
|
@ -396,7 +396,18 @@ pub fn extract_sprites(
|
|||
continue;
|
||||
}
|
||||
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
|
||||
let rect = Some(texture_atlas.textures[atlas_sprite.index]);
|
||||
let rect = Some(
|
||||
*texture_atlas
|
||||
.textures
|
||||
.get(atlas_sprite.index)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Sprite index {:?} does not exist for texture atlas handle {:?}.",
|
||||
atlas_sprite.index,
|
||||
texture_atlas_handle.id(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
extracted_sprites.sprites.push(ExtractedSprite {
|
||||
entity,
|
||||
color: atlas_sprite.color,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::Anchor;
|
||||
use bevy_asset::Handle;
|
||||
use bevy_ecs::component::Component;
|
||||
use bevy_ecs::{component::Component, reflect::ReflectComponent};
|
||||
use bevy_math::{Rect, Vec2};
|
||||
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
|
||||
use bevy_render::{color::Color, texture::Image};
|
||||
|
@ -23,7 +23,8 @@ pub struct TextureAtlas {
|
|||
pub(crate) texture_handles: Option<HashMap<Handle<Image>, usize>>,
|
||||
}
|
||||
|
||||
#[derive(Component, Debug, Clone, Reflect)]
|
||||
#[derive(Component, Debug, Clone, Reflect, FromReflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct TextureAtlasSprite {
|
||||
/// The tint color used to draw the sprite, defaulting to [`Color::WHITE`]
|
||||
pub color: Color,
|
||||
|
|
|
@ -572,7 +572,7 @@ impl Drop for TaskPool {
|
|||
}
|
||||
}
|
||||
|
||||
/// A `TaskPool` scope for running one or more non-`'static` futures.
|
||||
/// A [`TaskPool`] scope for running one or more non-`'static` futures.
|
||||
///
|
||||
/// For more information, see [`TaskPool::scope`].
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
|||
|
||||
[features]
|
||||
subpixel_glyph_atlas = []
|
||||
default_font = []
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
|
|
BIN
crates/bevy_text/src/FiraMono-subset.ttf
Normal file
BIN
crates/bevy_text/src/FiraMono-subset.ttf
Normal file
Binary file not shown.
|
@ -26,8 +26,11 @@ pub mod prelude {
|
|||
}
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::AddAsset;
|
||||
#[cfg(feature = "default_font")]
|
||||
use bevy_asset::load_internal_binary_asset;
|
||||
use bevy_asset::{AddAsset, HandleUntyped};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{camera::CameraUpdateSystem, ExtractSchedule, RenderApp};
|
||||
use bevy_sprite::SpriteSystem;
|
||||
use std::num::NonZeroUsize;
|
||||
|
@ -67,16 +70,20 @@ pub enum YAxisOrientation {
|
|||
BottomToTop,
|
||||
}
|
||||
|
||||
pub const DEFAULT_FONT_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Font::TYPE_UUID, 1491772431825224042);
|
||||
|
||||
impl Plugin for TextPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<Font>()
|
||||
.add_asset::<FontAtlasSet>()
|
||||
.register_type::<Text>()
|
||||
.register_type::<Text2dBounds>()
|
||||
.register_type::<TextSection>()
|
||||
.register_type::<Vec<TextSection>>()
|
||||
.register_type::<TextStyle>()
|
||||
.register_type::<Text>()
|
||||
.register_type::<TextAlignment>()
|
||||
.register_type::<BreakLineOn>()
|
||||
.init_asset_loader::<FontLoader>()
|
||||
.init_resource::<TextSettings>()
|
||||
.init_resource::<FontAtlasWarning>()
|
||||
|
@ -97,5 +104,13 @@ impl Plugin for TextPlugin {
|
|||
extract_text2d_sprite.after(SpriteSystem::ExtractSprites),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "default_font")]
|
||||
load_internal_binary_asset!(
|
||||
app,
|
||||
DEFAULT_FONT_HANDLE,
|
||||
"FiraMono-subset.ttf",
|
||||
|bytes: &[u8]| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use bevy_render::texture::Image;
|
|||
use bevy_sprite::TextureAtlas;
|
||||
use bevy_utils::HashMap;
|
||||
|
||||
use glyph_brush_layout::{FontId, SectionText};
|
||||
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText};
|
||||
|
||||
use crate::{
|
||||
error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
|
||||
|
@ -54,7 +54,7 @@ impl TextPipeline {
|
|||
font_atlas_warning: &mut FontAtlasWarning,
|
||||
y_axis_orientation: YAxisOrientation,
|
||||
) -> Result<TextLayoutInfo, TextError> {
|
||||
let mut scaled_fonts = Vec::new();
|
||||
let mut scaled_fonts = Vec::with_capacity(sections.len());
|
||||
let sections = sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
|
@ -92,6 +92,9 @@ impl TextPipeline {
|
|||
for sg in §ion_glyphs {
|
||||
let scaled_font = scaled_fonts[sg.section_index];
|
||||
let glyph = &sg.glyph;
|
||||
// The fonts use a coordinate system increasing upwards so ascent is a positive value
|
||||
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
|
||||
// so we have to subtract from the baseline position to get the minimum and maximum values.
|
||||
min_x = min_x.min(glyph.position.x);
|
||||
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
|
||||
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
|
||||
|
@ -114,4 +117,140 @@ impl TextPipeline {
|
|||
|
||||
Ok(TextLayoutInfo { glyphs, size })
|
||||
}
|
||||
|
||||
pub fn create_text_measure(
|
||||
&mut self,
|
||||
fonts: &Assets<Font>,
|
||||
sections: &[TextSection],
|
||||
scale_factor: f64,
|
||||
text_alignment: TextAlignment,
|
||||
linebreak_behaviour: BreakLineOn,
|
||||
) -> Result<TextMeasureInfo, TextError> {
|
||||
let mut auto_fonts = Vec::with_capacity(sections.len());
|
||||
let mut scaled_fonts = Vec::with_capacity(sections.len());
|
||||
let sections = sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, section)| {
|
||||
let font = fonts
|
||||
.get(§ion.style.font)
|
||||
.ok_or(TextError::NoSuchFont)?;
|
||||
let font_size = scale_value(section.style.font_size, scale_factor);
|
||||
auto_fonts.push(font.font.clone());
|
||||
let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size);
|
||||
scaled_fonts.push(px_scale_font);
|
||||
|
||||
let section = TextMeasureSection {
|
||||
font_id: FontId(i),
|
||||
scale: PxScale::from(font_size),
|
||||
text: section.value.clone(),
|
||||
};
|
||||
|
||||
Ok(section)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(TextMeasureInfo::new(
|
||||
auto_fonts,
|
||||
scaled_fonts,
|
||||
sections,
|
||||
text_alignment,
|
||||
linebreak_behaviour.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextMeasureSection {
|
||||
pub text: String,
|
||||
pub scale: PxScale,
|
||||
pub font_id: FontId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextMeasureInfo {
|
||||
pub fonts: Vec<ab_glyph::FontArc>,
|
||||
pub scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
|
||||
pub sections: Vec<TextMeasureSection>,
|
||||
pub text_alignment: TextAlignment,
|
||||
pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
|
||||
pub min_width_content_size: Vec2,
|
||||
pub max_width_content_size: Vec2,
|
||||
}
|
||||
|
||||
impl TextMeasureInfo {
|
||||
fn new(
|
||||
fonts: Vec<ab_glyph::FontArc>,
|
||||
scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
|
||||
sections: Vec<TextMeasureSection>,
|
||||
text_alignment: TextAlignment,
|
||||
linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
|
||||
) -> Self {
|
||||
let mut info = Self {
|
||||
fonts,
|
||||
scaled_fonts,
|
||||
sections,
|
||||
text_alignment,
|
||||
linebreak_behaviour,
|
||||
min_width_content_size: Vec2::ZERO,
|
||||
max_width_content_size: Vec2::ZERO,
|
||||
};
|
||||
|
||||
let section_texts = info.prepare_section_texts();
|
||||
let min =
|
||||
info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY));
|
||||
let max = info.compute_size_from_section_texts(
|
||||
§ion_texts,
|
||||
Vec2::new(f32::INFINITY, f32::INFINITY),
|
||||
);
|
||||
info.min_width_content_size = min;
|
||||
info.max_width_content_size = max;
|
||||
info
|
||||
}
|
||||
|
||||
fn prepare_section_texts(&self) -> Vec<SectionText> {
|
||||
self.sections
|
||||
.iter()
|
||||
.map(|section| SectionText {
|
||||
font_id: section.font_id,
|
||||
scale: section.scale,
|
||||
text: §ion.text,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 {
|
||||
let geom = SectionGeometry {
|
||||
bounds: (bounds.x, bounds.y),
|
||||
..Default::default()
|
||||
};
|
||||
let section_glyphs = glyph_brush_layout::Layout::default()
|
||||
.h_align(self.text_alignment.into())
|
||||
.line_breaker(self.linebreak_behaviour)
|
||||
.calculate_glyphs(&self.fonts, &geom, sections);
|
||||
|
||||
let mut min_x: f32 = std::f32::MAX;
|
||||
let mut min_y: f32 = std::f32::MAX;
|
||||
let mut max_x: f32 = std::f32::MIN;
|
||||
let mut max_y: f32 = std::f32::MIN;
|
||||
|
||||
for sg in section_glyphs {
|
||||
let scaled_font = &self.scaled_fonts[sg.section_index];
|
||||
let glyph = &sg.glyph;
|
||||
// The fonts use a coordinate system increasing upwards so ascent is a positive value
|
||||
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
|
||||
// so we have to subtract from the baseline position to get the minimum and maximum values.
|
||||
min_x = min_x.min(glyph.position.x);
|
||||
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
|
||||
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
|
||||
max_y = max_y.max(glyph.position.y - scaled_font.descent());
|
||||
}
|
||||
|
||||
Vec2::new(max_x - min_x, max_y - min_y)
|
||||
}
|
||||
|
||||
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
|
||||
let sections = self.prepare_section_texts();
|
||||
self.compute_size_from_section_texts(§ions, bounds)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use bevy_render::color::Color;
|
|||
use bevy_utils::default;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Font;
|
||||
use crate::{Font, DEFAULT_FONT_HANDLE};
|
||||
|
||||
#[derive(Component, Debug, Clone, Reflect)]
|
||||
#[reflect(Component, Default)]
|
||||
|
@ -167,7 +167,7 @@ pub struct TextStyle {
|
|||
impl Default for TextStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font: Default::default(),
|
||||
font: DEFAULT_FONT_HANDLE.typed(),
|
||||
font_size: 12.0,
|
||||
color: Color::WHITE,
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use bevy_ecs::{
|
|||
event::EventReader,
|
||||
prelude::With,
|
||||
reflect::ReflectComponent,
|
||||
system::{Commands, Local, Query, Res, ResMut},
|
||||
system::{Local, Query, Res, ResMut},
|
||||
};
|
||||
use bevy_math::{Vec2, Vec3};
|
||||
use bevy_reflect::Reflect;
|
||||
|
@ -29,7 +29,7 @@ use crate::{
|
|||
|
||||
/// The maximum width and height of text. The text will wrap according to the specified size.
|
||||
/// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the
|
||||
/// specified `TextAlignment`.
|
||||
/// specified [`TextAlignment`](crate::text::TextAlignment).
|
||||
///
|
||||
/// Note: only characters that are completely out of the bounds will be truncated, so this is not a
|
||||
/// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this
|
||||
|
@ -72,6 +72,8 @@ pub struct Text2dBundle {
|
|||
pub visibility: Visibility,
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
|
||||
pub computed_visibility: ComputedVisibility,
|
||||
/// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`]
|
||||
pub text_layout_info: TextLayoutInfo,
|
||||
}
|
||||
|
||||
pub fn extract_text2d_sprite(
|
||||
|
@ -147,7 +149,6 @@ pub fn extract_text2d_sprite(
|
|||
/// It does not modify or observe existing ones.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_text2d_layout(
|
||||
mut commands: Commands,
|
||||
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
|
||||
mut queue: Local<HashSet<Entity>>,
|
||||
mut textures: ResMut<Assets<Image>>,
|
||||
|
@ -159,12 +160,7 @@ pub fn update_text2d_layout(
|
|||
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
|
||||
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
|
||||
mut text_pipeline: ResMut<TextPipeline>,
|
||||
mut text_query: Query<(
|
||||
Entity,
|
||||
Ref<Text>,
|
||||
Ref<Text2dBounds>,
|
||||
Option<&mut TextLayoutInfo>,
|
||||
)>,
|
||||
mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &mut TextLayoutInfo)>,
|
||||
) {
|
||||
// We need to consume the entire iterator, hence `last`
|
||||
let factor_changed = scale_factor_changed.iter().last().is_some();
|
||||
|
@ -175,7 +171,7 @@ pub fn update_text2d_layout(
|
|||
.map(|window| window.resolution.scale_factor())
|
||||
.unwrap_or(1.0);
|
||||
|
||||
for (entity, text, bounds, text_layout_info) in &mut text_query {
|
||||
for (entity, text, bounds, mut text_layout_info) in &mut text_query {
|
||||
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
|
||||
let text_bounds = Vec2::new(
|
||||
scale_value(bounds.size.x, scale_factor),
|
||||
|
@ -204,12 +200,7 @@ pub fn update_text2d_layout(
|
|||
Err(e @ TextError::FailedToAddGlyph(_)) => {
|
||||
panic!("Fatal error when processing text: {e}.");
|
||||
}
|
||||
Ok(info) => match text_layout_info {
|
||||
Some(mut t) => *t = info,
|
||||
None => {
|
||||
commands.entity(entity).insert(info);
|
||||
}
|
||||
},
|
||||
Ok(info) => *text_layout_info = info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,10 +224,17 @@ impl Timer {
|
|||
|
||||
if self.finished() {
|
||||
if self.mode == TimerMode::Repeating {
|
||||
self.times_finished_this_tick =
|
||||
(self.elapsed().as_nanos() / self.duration().as_nanos()) as u32;
|
||||
// Duration does not have a modulo
|
||||
self.set_elapsed(self.elapsed() - self.duration() * self.times_finished_this_tick);
|
||||
self.times_finished_this_tick = self
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.checked_div(self.duration().as_nanos())
|
||||
.map_or(u32::MAX, |x| x as u32);
|
||||
self.set_elapsed(
|
||||
self.elapsed()
|
||||
.as_nanos()
|
||||
.checked_rem(self.duration().as_nanos())
|
||||
.map_or(Duration::ZERO, |x| Duration::from_nanos(x as u64)),
|
||||
);
|
||||
} else {
|
||||
self.times_finished_this_tick = 1;
|
||||
self.set_elapsed(self.duration());
|
||||
|
@ -329,7 +336,11 @@ impl Timer {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn percent(&self) -> f32 {
|
||||
self.elapsed().as_secs_f32() / self.duration().as_secs_f32()
|
||||
if self.duration == Duration::ZERO {
|
||||
1.0
|
||||
} else {
|
||||
self.elapsed().as_secs_f32() / self.duration().as_secs_f32()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the percentage of the timer remaining time (goes from 1.0 to 0.0).
|
||||
|
@ -517,6 +528,26 @@ mod tests {
|
|||
assert_eq!(t.times_finished_this_tick(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn times_finished_this_tick_repeating_zero_duration() {
|
||||
let mut t = Timer::from_seconds(0.0, TimerMode::Repeating);
|
||||
assert_eq!(t.times_finished_this_tick(), 0);
|
||||
assert_eq!(t.elapsed(), Duration::ZERO);
|
||||
assert_eq!(t.percent(), 1.0);
|
||||
t.tick(Duration::from_secs(1));
|
||||
assert_eq!(t.times_finished_this_tick(), u32::MAX);
|
||||
assert_eq!(t.elapsed(), Duration::ZERO);
|
||||
assert_eq!(t.percent(), 1.0);
|
||||
t.tick(Duration::from_secs(2));
|
||||
assert_eq!(t.times_finished_this_tick(), u32::MAX);
|
||||
assert_eq!(t.elapsed(), Duration::ZERO);
|
||||
assert_eq!(t.percent(), 1.0);
|
||||
t.reset();
|
||||
assert_eq!(t.times_finished_this_tick(), 0);
|
||||
assert_eq!(t.elapsed(), Duration::ZERO);
|
||||
assert_eq!(t.percent(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn times_finished_this_tick_precise() {
|
||||
let mut t = Timer::from_seconds(0.01, TimerMode::Repeating);
|
||||
|
|
|
@ -45,7 +45,7 @@ impl Command for AddChildInPlace {
|
|||
/// You most likely want to use [`BuildChildrenTransformExt::remove_parent_in_place`]
|
||||
/// method on [`EntityCommands`] instead.
|
||||
pub struct RemoveParentInPlace {
|
||||
/// `Entity` whose parent must be removed.
|
||||
/// [`Entity`] whose parent must be removed.
|
||||
pub child: Entity,
|
||||
}
|
||||
impl Command for RemoveParentInPlace {
|
||||
|
|
|
@ -111,9 +111,9 @@ impl GlobalTransform {
|
|||
/// Returns the [`Transform`] `self` would have if it was a child of an entity
|
||||
/// with the `parent` [`GlobalTransform`].
|
||||
///
|
||||
/// This is useful if you want to "reparent" an `Entity`. Say you have an entity
|
||||
/// `e1` that you want to turn into a child of `e2`, but you want `e1` to keep the
|
||||
/// same global transform, even after re-parenting. You would use:
|
||||
/// This is useful if you want to "reparent" an [`Entity`](bevy_ecs::entity::Entity).
|
||||
/// Say you have an entity `e1` that you want to turn into a child of `e2`,
|
||||
/// but you want `e1` to keep the same global transform, even after re-parenting. You would use:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue