Only use physical coords internally in bevy_ui
(#16375)
# Objective We switch back and forwards between logical and physical coordinates all over the place. Systems have to query for cameras and the UiScale when they shouldn't need to. It's confusing and fragile and new scale factor bugs get found constantly. ## Solution * Use physical coordinates whereever possible in `bevy_ui`. * Store physical coords in `ComputedNode` and tear out all the unneeded scale factor calculations and queries. * Add an `inverse_scale_factor` field to `ComputedNode` and set nodes changed when their scale factor changes. ## Migration Guide `ComputedNode`'s fields and methods now use physical coordinates. `ComputedNode` has a new field `inverse_scale_factor`. Multiplying the physical coordinates by the `inverse_scale_factor` will give the logical values. --------- Co-authored-by: atlv <email@atlasdostal.com>
This commit is contained in:
parent
513be52505
commit
8a3a8b5cfb
@ -1,6 +1,5 @@
|
||||
use crate::{
|
||||
CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, TargetCamera, UiScale,
|
||||
UiStack,
|
||||
CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, TargetCamera, UiStack,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChangesMut,
|
||||
@ -158,7 +157,6 @@ pub fn ui_focus_system(
|
||||
windows: Query<&Window>,
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
touches_input: Res<Touches>,
|
||||
ui_scale: Res<UiScale>,
|
||||
ui_stack: Res<UiStack>,
|
||||
mut node_query: Query<NodeQuery>,
|
||||
) {
|
||||
@ -201,19 +199,16 @@ pub fn ui_focus_system(
|
||||
};
|
||||
|
||||
let viewport_position = camera
|
||||
.logical_viewport_rect()
|
||||
.map(|rect| rect.min)
|
||||
.physical_viewport_rect()
|
||||
.map(|rect| rect.min.as_vec2())
|
||||
.unwrap_or_default();
|
||||
windows
|
||||
.get(window_ref.entity())
|
||||
.ok()
|
||||
.and_then(Window::cursor_position)
|
||||
.and_then(Window::physical_cursor_position)
|
||||
.or_else(|| touches_input.first_pressed_position())
|
||||
.map(|cursor_position| (entity, cursor_position - viewport_position))
|
||||
})
|
||||
// The cursor position returned by `Window` only takes into account the window scale factor and not `UiScale`.
|
||||
// To convert the cursor position to logical UI viewport coordinates we have to divide it by `UiScale`.
|
||||
.map(|(entity, cursor_position)| (entity, cursor_position / ui_scale.0))
|
||||
.collect();
|
||||
|
||||
// prepare an iterator that contains all the nodes that have the cursor in their rect,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
experimental::{UiChildren, UiRootNodes},
|
||||
BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
|
||||
ScrollPosition, TargetCamera, UiScale,
|
||||
ScrollPosition, TargetCamera, UiScale, Val,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
change_detection::{DetectChanges, DetectChangesMut},
|
||||
@ -343,31 +343,33 @@ with UI components as a child of an entity without UI components, your UI layout
|
||||
maybe_scroll_position,
|
||||
)) = node_transform_query.get_mut(entity)
|
||||
{
|
||||
let Ok((layout, unrounded_unscaled_size)) = ui_surface.get_layout(entity) else {
|
||||
let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let layout_size =
|
||||
inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height);
|
||||
let unrounded_size = inverse_target_scale_factor * unrounded_unscaled_size;
|
||||
let layout_location =
|
||||
inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y);
|
||||
let layout_size = Vec2::new(layout.size.width, layout.size.height);
|
||||
|
||||
let layout_location = Vec2::new(layout.location.x, layout.location.y);
|
||||
|
||||
// The position of the center of the node, stored in the node's transform
|
||||
let node_center =
|
||||
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
|
||||
|
||||
// only trigger change detection when the new values are different
|
||||
if node.size != layout_size || node.unrounded_size != unrounded_size {
|
||||
if node.size != layout_size
|
||||
|| node.unrounded_size != unrounded_size
|
||||
|| node.inverse_scale_factor != inverse_target_scale_factor
|
||||
{
|
||||
node.size = layout_size;
|
||||
node.unrounded_size = unrounded_size;
|
||||
node.inverse_scale_factor = inverse_target_scale_factor;
|
||||
}
|
||||
|
||||
let taffy_rect_to_border_rect = |rect: taffy::Rect<f32>| BorderRect {
|
||||
left: rect.left * inverse_target_scale_factor,
|
||||
right: rect.right * inverse_target_scale_factor,
|
||||
top: rect.top * inverse_target_scale_factor,
|
||||
bottom: rect.bottom * inverse_target_scale_factor,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
};
|
||||
|
||||
node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border);
|
||||
@ -377,28 +379,35 @@ with UI components as a child of an entity without UI components, your UI layout
|
||||
|
||||
if let Some(border_radius) = maybe_border_radius {
|
||||
// We don't trigger change detection for changes to border radius
|
||||
node.bypass_change_detection().border_radius =
|
||||
border_radius.resolve(node.size, viewport_size);
|
||||
node.bypass_change_detection().border_radius = border_radius.resolve(
|
||||
node.size,
|
||||
viewport_size,
|
||||
inverse_target_scale_factor.recip(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(outline) = maybe_outline {
|
||||
// don't trigger change detection when only outlines are changed
|
||||
let node = node.bypass_change_detection();
|
||||
node.outline_width = if style.display != Display::None {
|
||||
outline
|
||||
.width
|
||||
.resolve(node.size().x, viewport_size)
|
||||
.unwrap_or(0.)
|
||||
.max(0.)
|
||||
match outline.width {
|
||||
Val::Px(w) => Val::Px(w / inverse_target_scale_factor),
|
||||
width => width,
|
||||
}
|
||||
.resolve(node.size().x, viewport_size)
|
||||
.unwrap_or(0.)
|
||||
.max(0.)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
node.outline_offset = outline
|
||||
.offset
|
||||
.resolve(node.size().x, viewport_size)
|
||||
.unwrap_or(0.)
|
||||
.max(0.);
|
||||
node.outline_offset = match outline.offset {
|
||||
Val::Px(offset) => Val::Px(offset / inverse_target_scale_factor),
|
||||
offset => offset,
|
||||
}
|
||||
.resolve(node.size().x, viewport_size)
|
||||
.unwrap_or(0.)
|
||||
.max(0.);
|
||||
}
|
||||
|
||||
if transform.translation.truncate() != node_center {
|
||||
@ -422,8 +431,7 @@ with UI components as a child of an entity without UI components, your UI layout
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let content_size = Vec2::new(layout.content_size.width, layout.content_size.height)
|
||||
* inverse_target_scale_factor;
|
||||
let content_size = Vec2::new(layout.content_size.width, layout.content_size.height);
|
||||
let max_possible_offset = (content_size - layout_size).max(Vec2::ZERO);
|
||||
let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);
|
||||
|
||||
@ -1131,7 +1139,7 @@ mod tests {
|
||||
.sum();
|
||||
let parent_width = world.get::<ComputedNode>(parent).unwrap().size.x;
|
||||
assert!((width_sum - parent_width).abs() < 0.001);
|
||||
assert!((width_sum - 320.).abs() <= 1.);
|
||||
assert!((width_sum - 320. * s).abs() <= 1.);
|
||||
s += r;
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,6 @@ pub fn ui_picking(
|
||||
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
ui_scale: Res<UiScale>,
|
||||
ui_stack: Res<UiStack>,
|
||||
node_query: Query<NodeQuery>,
|
||||
mut output: EventWriter<PointerHits>,
|
||||
@ -95,15 +94,15 @@ pub fn ui_picking(
|
||||
let Ok((_, camera_data, _)) = camera_query.get(camera) else {
|
||||
continue;
|
||||
};
|
||||
let mut pointer_pos = pointer_location.position;
|
||||
if let Some(viewport) = camera_data.logical_viewport_rect() {
|
||||
pointer_pos -= viewport.min;
|
||||
let mut pointer_pos =
|
||||
pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.);
|
||||
if let Some(viewport) = camera_data.physical_viewport_rect() {
|
||||
pointer_pos -= viewport.min.as_vec2();
|
||||
}
|
||||
let scaled_pointer_pos = pointer_pos / **ui_scale;
|
||||
pointer_pos_by_camera
|
||||
.entry(camera)
|
||||
.or_default()
|
||||
.insert(pointer_id, scaled_pointer_pos);
|
||||
.insert(pointer_id, pointer_pos);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use core::{hash::Hash, ops::Range};
|
||||
|
||||
use crate::{
|
||||
BoxShadow, CalculatedClip, ComputedNode, DefaultUiCamera, RenderUiSystem, ResolvedBorderRadius,
|
||||
TargetCamera, TransparentUi, UiBoxShadowSamples, UiScale, Val,
|
||||
TargetCamera, TransparentUi, UiBoxShadowSamples, Val,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::*;
|
||||
@ -237,7 +237,6 @@ pub fn extract_shadows(
|
||||
mut commands: Commands,
|
||||
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
camera_query: Extract<Query<(Entity, &Camera)>>,
|
||||
box_shadow_query: Extract<
|
||||
Query<(
|
||||
@ -268,37 +267,36 @@ pub fn extract_shadows(
|
||||
continue;
|
||||
}
|
||||
|
||||
let ui_logical_viewport_size = camera_query
|
||||
let ui_physical_viewport_size = camera_query
|
||||
.get(camera_entity)
|
||||
.ok()
|
||||
.and_then(|(_, c)| c.logical_viewport_size())
|
||||
.unwrap_or(Vec2::ZERO)
|
||||
// The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`,
|
||||
// so we have to divide by `UiScale` to get the size of the UI viewport.
|
||||
/ ui_scale.0;
|
||||
.and_then(|(_, c)| {
|
||||
c.physical_viewport_size()
|
||||
.map(|size| Vec2::new(size.x as f32, size.y as f32))
|
||||
})
|
||||
.unwrap_or(Vec2::ZERO);
|
||||
|
||||
let resolve_val = |val, base| match val {
|
||||
let scale_factor = uinode.inverse_scale_factor.recip();
|
||||
|
||||
let resolve_val = |val, base, scale_factor| match val {
|
||||
Val::Auto => 0.,
|
||||
Val::Px(px) => px,
|
||||
Val::Px(px) => px * scale_factor,
|
||||
Val::Percent(percent) => percent / 100. * base,
|
||||
Val::Vw(percent) => percent / 100. * ui_logical_viewport_size.x,
|
||||
Val::Vh(percent) => percent / 100. * ui_logical_viewport_size.y,
|
||||
Val::VMin(percent) => percent / 100. * ui_logical_viewport_size.min_element(),
|
||||
Val::VMax(percent) => percent / 100. * ui_logical_viewport_size.max_element(),
|
||||
Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x,
|
||||
Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y,
|
||||
Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(),
|
||||
Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(),
|
||||
};
|
||||
|
||||
let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x);
|
||||
let spread_ratio_x = (spread_x + uinode.size().x) / uinode.size().x;
|
||||
let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x, scale_factor);
|
||||
let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x;
|
||||
|
||||
let spread = vec2(
|
||||
spread_x,
|
||||
(spread_ratio_x * uinode.size().y) - uinode.size().y,
|
||||
);
|
||||
let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y);
|
||||
|
||||
let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x);
|
||||
let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x, scale_factor);
|
||||
let offset = vec2(
|
||||
resolve_val(box_shadow.x_offset, uinode.size().x),
|
||||
resolve_val(box_shadow.y_offset, uinode.size().y),
|
||||
resolve_val(box_shadow.x_offset, uinode.size().x, scale_factor),
|
||||
resolve_val(box_shadow.y_offset, uinode.size().y, scale_factor),
|
||||
);
|
||||
|
||||
let shadow_size = uinode.size() + spread;
|
||||
@ -307,10 +305,10 @@ pub fn extract_shadows(
|
||||
}
|
||||
|
||||
let radius = ResolvedBorderRadius {
|
||||
top_left: uinode.border_radius.top_left * spread_ratio_x,
|
||||
top_right: uinode.border_radius.top_right * spread_ratio_x,
|
||||
bottom_left: uinode.border_radius.bottom_left * spread_ratio_x,
|
||||
bottom_right: uinode.border_radius.bottom_right * spread_ratio_x,
|
||||
top_left: uinode.border_radius.top_left * spread_ratio,
|
||||
top_right: uinode.border_radius.top_right * spread_ratio,
|
||||
bottom_left: uinode.border_radius.bottom_left * spread_ratio,
|
||||
bottom_right: uinode.border_radius.bottom_right * spread_ratio,
|
||||
};
|
||||
|
||||
extracted_box_shadows.box_shadows.insert(
|
||||
@ -373,7 +371,6 @@ pub fn queue_shadows(
|
||||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ use crate::widget::ImageNode;
|
||||
use crate::{
|
||||
experimental::UiChildren, BackgroundColor, BorderColor, CalculatedClip, ComputedNode,
|
||||
DefaultUiCamera, Outline, ResolvedBorderRadius, TargetCamera, UiAntiAlias, UiBoxShadowSamples,
|
||||
UiScale,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
|
||||
@ -19,7 +18,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
|
||||
use bevy_ecs::entity::{EntityHashMap, EntityHashSet};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_image::Image;
|
||||
use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles};
|
||||
use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles};
|
||||
use bevy_render::render_phase::ViewSortedRenderPhases;
|
||||
use bevy_render::sync_world::MainEntity;
|
||||
use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE;
|
||||
@ -528,16 +527,10 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
|
||||
#[derive(Component)]
|
||||
pub struct DefaultCameraView(pub Entity);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ExtractedAA {
|
||||
pub scale_factor: f32,
|
||||
}
|
||||
|
||||
/// Extracts all UI elements associated with a camera into the render world.
|
||||
pub fn extract_default_ui_camera_view(
|
||||
mut commands: Commands,
|
||||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
query: Extract<
|
||||
Query<
|
||||
(
|
||||
@ -553,41 +546,26 @@ pub fn extract_default_ui_camera_view(
|
||||
) {
|
||||
live_entities.clear();
|
||||
|
||||
let scale = ui_scale.0.recip();
|
||||
for (entity, camera, ui_anti_alias, shadow_samples) in &query {
|
||||
// ignore inactive cameras
|
||||
if !camera.is_active {
|
||||
commands
|
||||
.get_entity(entity)
|
||||
.expect("Camera entity wasn't synced.")
|
||||
.remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>();
|
||||
.remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let (
|
||||
Some(logical_size),
|
||||
Some(URect {
|
||||
min: physical_origin,
|
||||
..
|
||||
}),
|
||||
Some(physical_size),
|
||||
Some(scale_factor),
|
||||
) = (
|
||||
camera.logical_viewport_size(),
|
||||
camera.physical_viewport_rect(),
|
||||
camera.physical_viewport_size(),
|
||||
camera.target_scaling_factor(),
|
||||
) {
|
||||
if let Some(physical_viewport_rect) = camera.physical_viewport_rect() {
|
||||
// use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection
|
||||
let projection_matrix = Mat4::orthographic_rh(
|
||||
0.0,
|
||||
logical_size.x * scale,
|
||||
logical_size.y * scale,
|
||||
physical_viewport_rect.width() as f32,
|
||||
physical_viewport_rect.height() as f32,
|
||||
0.0,
|
||||
0.0,
|
||||
UI_CAMERA_FAR,
|
||||
);
|
||||
|
||||
let default_camera_view = commands
|
||||
.spawn((
|
||||
ExtractedView {
|
||||
@ -599,12 +577,10 @@ pub fn extract_default_ui_camera_view(
|
||||
),
|
||||
clip_from_world: None,
|
||||
hdr: camera.hdr,
|
||||
viewport: UVec4::new(
|
||||
physical_origin.x,
|
||||
physical_origin.y,
|
||||
physical_size.x,
|
||||
physical_size.y,
|
||||
),
|
||||
viewport: UVec4::from((
|
||||
physical_viewport_rect.min,
|
||||
physical_viewport_rect.size(),
|
||||
)),
|
||||
color_grading: Default::default(),
|
||||
},
|
||||
TemporaryRenderEntity,
|
||||
@ -614,10 +590,8 @@ pub fn extract_default_ui_camera_view(
|
||||
.get_entity(entity)
|
||||
.expect("Camera entity wasn't synced.");
|
||||
entity_commands.insert(DefaultCameraView(default_camera_view));
|
||||
if ui_anti_alias != Some(&UiAntiAlias::Off) {
|
||||
entity_commands.insert(ExtractedAA {
|
||||
scale_factor: (scale_factor * ui_scale.0),
|
||||
});
|
||||
if let Some(ui_anti_alias) = ui_anti_alias {
|
||||
entity_commands.insert(*ui_anti_alias);
|
||||
}
|
||||
if let Some(shadow_samples) = shadow_samples {
|
||||
entity_commands.insert(*shadow_samples);
|
||||
@ -635,10 +609,8 @@ pub fn extract_default_ui_camera_view(
|
||||
pub fn extract_text_sections(
|
||||
mut commands: Commands,
|
||||
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
|
||||
camera_query: Extract<Query<&Camera>>,
|
||||
default_ui_camera: Extract<DefaultUiCamera>,
|
||||
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
|
||||
ui_scale: Extract<Res<UiScale>>,
|
||||
uinode_query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
@ -678,32 +650,18 @@ pub fn extract_text_sections(
|
||||
continue;
|
||||
}
|
||||
|
||||
let scale_factor = camera_query
|
||||
.get(camera_entity)
|
||||
.ok()
|
||||
.and_then(Camera::target_scaling_factor)
|
||||
.unwrap_or(1.0)
|
||||
* ui_scale.0;
|
||||
let inverse_scale_factor = scale_factor.recip();
|
||||
|
||||
let Ok(&render_camera_entity) = mapping.get(camera_entity) else {
|
||||
continue;
|
||||
};
|
||||
// Align the text to the nearest physical pixel:
|
||||
|
||||
// Align the text to the nearest pixel:
|
||||
// * Translate by minus the text node's half-size
|
||||
// (The transform translates to the center of the node but the text coordinates are relative to the node's top left corner)
|
||||
// * Multiply the logical coordinates by the scale factor to get its position in physical coordinates
|
||||
// * Round the physical position to the nearest physical pixel
|
||||
// * Multiply by the rounded physical position by the inverse scale factor to return to logical coordinates
|
||||
|
||||
let logical_top_left = -0.5 * uinode.size();
|
||||
// * Round the position to the nearest physical pixel
|
||||
|
||||
let mut transform = global_transform.affine()
|
||||
* bevy_math::Affine3A::from_translation(logical_top_left.extend(0.));
|
||||
|
||||
transform.translation *= scale_factor;
|
||||
* bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.));
|
||||
transform.translation = transform.translation.round();
|
||||
transform.translation *= inverse_scale_factor;
|
||||
|
||||
let mut color = LinearRgba::WHITE;
|
||||
let mut current_span = usize::MAX;
|
||||
@ -730,15 +688,14 @@ pub fn extract_text_sections(
|
||||
.unwrap_or_default();
|
||||
current_span = *span_index;
|
||||
}
|
||||
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
|
||||
|
||||
let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect();
|
||||
rect.min *= inverse_scale_factor;
|
||||
rect.max *= inverse_scale_factor;
|
||||
|
||||
let rect = texture_atlases
|
||||
.get(&atlas_info.texture_atlas)
|
||||
.unwrap()
|
||||
.textures[atlas_info.location.glyph_index]
|
||||
.as_rect();
|
||||
extracted_uinodes.glyphs.push(ExtractedGlyph {
|
||||
transform: transform
|
||||
* Mat4::from_translation(position.extend(0.) * inverse_scale_factor),
|
||||
transform: transform * Mat4::from_translation(position.extend(0.)),
|
||||
rect,
|
||||
});
|
||||
|
||||
@ -762,7 +719,7 @@ pub fn extract_text_sections(
|
||||
camera_entity: render_camera_entity.id(),
|
||||
rect,
|
||||
item: ExtractedUiItem::Glyphs {
|
||||
atlas_scaling: Vec2::splat(inverse_scale_factor),
|
||||
atlas_scaling: Vec2::ONE,
|
||||
range: start..end,
|
||||
},
|
||||
main_entity: entity.into(),
|
||||
@ -795,7 +752,6 @@ struct UiVertex {
|
||||
pub size: [f32; 2],
|
||||
/// Position relative to the center of the UI node.
|
||||
pub point: [f32; 2],
|
||||
pub inverse_scale_factor: f32,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
@ -846,13 +802,13 @@ pub fn queue_uinodes(
|
||||
ui_pipeline: Res<UiPipeline>,
|
||||
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
|
||||
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
|
||||
mut views: Query<(Entity, &ExtractedView, Option<&ExtractedAA>)>,
|
||||
mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
draw_functions: Res<DrawFunctions<TransparentUi>>,
|
||||
) {
|
||||
let draw_function = draw_functions.read().id::<DrawUi>();
|
||||
for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() {
|
||||
let Ok((view_entity, view, extracted_aa)) = views.get_mut(extracted_uinode.camera_entity)
|
||||
let Ok((view_entity, view, ui_anti_alias)) = views.get_mut(extracted_uinode.camera_entity)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@ -866,7 +822,7 @@ pub fn queue_uinodes(
|
||||
&ui_pipeline,
|
||||
UiPipelineKey {
|
||||
hdr: view.hdr,
|
||||
anti_alias: extracted_aa.is_some(),
|
||||
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
|
||||
},
|
||||
);
|
||||
transparent_phase.add(TransparentUi {
|
||||
@ -880,7 +836,6 @@ pub fn queue_uinodes(
|
||||
// batch_range will be calculated in prepare_uinodes
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: extracted_aa.map(|aa| aa.scale_factor).unwrap_or(1.),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1151,7 +1106,6 @@ pub fn prepare_uinodes(
|
||||
border: [border.left, border.top, border.right, border.bottom],
|
||||
size: rect_size.xy().into(),
|
||||
point: points[i].into(),
|
||||
inverse_scale_factor: item.inverse_scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1255,7 +1209,6 @@ pub fn prepare_uinodes(
|
||||
border: [0.0; 4],
|
||||
size: size.into(),
|
||||
point: [0.0; 2],
|
||||
inverse_scale_factor: item.inverse_scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,6 @@ impl SpecializedRenderPipeline for UiPipeline {
|
||||
VertexFormat::Float32x2,
|
||||
// position relative to the center
|
||||
VertexFormat::Float32x2,
|
||||
// inverse scale factor
|
||||
VertexFormat::Float32,
|
||||
],
|
||||
);
|
||||
let shader_defs = if key.anti_alias {
|
||||
|
@ -97,7 +97,6 @@ pub struct TransparentUi {
|
||||
pub draw_function: DrawFunctionId,
|
||||
pub batch_range: Range<u32>,
|
||||
pub extra_index: PhaseItemExtraIndex,
|
||||
pub inverse_scale_factor: f32,
|
||||
}
|
||||
|
||||
impl PhaseItem for TransparentUi {
|
||||
|
@ -22,7 +22,6 @@ struct VertexOutput {
|
||||
|
||||
// Position relative to the center of the rectangle.
|
||||
@location(6) point: vec2<f32>,
|
||||
@location(7) @interpolate(flat) scale_factor: f32,
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
@ -40,7 +39,6 @@ fn vertex(
|
||||
@location(5) border: vec4<f32>,
|
||||
@location(6) size: vec2<f32>,
|
||||
@location(7) point: vec2<f32>,
|
||||
@location(8) scale_factor: f32,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.uv = vertex_uv;
|
||||
@ -51,7 +49,6 @@ fn vertex(
|
||||
out.size = size;
|
||||
out.border = border;
|
||||
out.point = point;
|
||||
out.scale_factor = scale_factor;
|
||||
|
||||
return out;
|
||||
}
|
||||
@ -118,9 +115,9 @@ fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, in
|
||||
}
|
||||
|
||||
// get alpha for antialiasing for sdf
|
||||
fn antialias(distance: f32, scale_factor: f32) -> f32 {
|
||||
fn antialias(distance: f32) -> f32 {
|
||||
// Using the fwidth(distance) was causing artifacts, so just use the distance.
|
||||
return clamp(0.0, 1.0, (0.5 - scale_factor * distance));
|
||||
return clamp(0.0, 1.0, (0.5 - distance));
|
||||
}
|
||||
|
||||
fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
|
||||
@ -151,7 +148,7 @@ fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
|
||||
// This select statement ensures we only perform anti-aliasing where a non-zero width border
|
||||
// is present, otherwise an outline about the external boundary would be drawn even without
|
||||
// a border.
|
||||
let t = select(1.0 - step(0.0, border_distance), antialias(border_distance, in.scale_factor), external_distance < internal_distance);
|
||||
let t = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance);
|
||||
#else
|
||||
let t = 1.0 - step(0.0, border_distance);
|
||||
#endif
|
||||
@ -167,7 +164,7 @@ fn draw_background(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
|
||||
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
|
||||
|
||||
#ifdef ANTI_ALIAS
|
||||
let t = antialias(internal_distance, in.scale_factor);
|
||||
let t = antialias(internal_distance);
|
||||
#else
|
||||
let t = 1.0 - step(0.0, internal_distance);
|
||||
#endif
|
||||
|
@ -655,7 +655,6 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
|
||||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -237,6 +237,7 @@ pub struct ExtractedUiTextureSlice {
|
||||
pub image_scale_mode: SpriteImageMode,
|
||||
pub flip_x: bool,
|
||||
pub flip_y: bool,
|
||||
pub inverse_scale_factor: f32,
|
||||
pub main_entity: MainEntity,
|
||||
}
|
||||
|
||||
@ -331,6 +332,7 @@ pub fn extract_ui_texture_slices(
|
||||
atlas_rect,
|
||||
flip_x: image.flip_x,
|
||||
flip_y: image.flip_y,
|
||||
inverse_scale_factor: uinode.inverse_scale_factor,
|
||||
main_entity: entity.into(),
|
||||
},
|
||||
);
|
||||
@ -372,7 +374,6 @@ pub fn queue_ui_slices(
|
||||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -609,7 +610,7 @@ pub fn prepare_ui_slices(
|
||||
|
||||
let [slices, border, repeat] = compute_texture_slices(
|
||||
image_size,
|
||||
uinode_rect.size(),
|
||||
uinode_rect.size() * texture_slices.inverse_scale_factor,
|
||||
&texture_slices.image_scale_mode,
|
||||
);
|
||||
|
||||
|
@ -22,7 +22,7 @@ pub struct ComputedNode {
|
||||
/// The order of the node in the UI layout.
|
||||
/// Nodes with a higher stack index are drawn on top of and receive interactions before nodes with lower stack indices.
|
||||
pub(crate) stack_index: u32,
|
||||
/// The size of the node as width and height in logical pixels
|
||||
/// The size of the node as width and height in physical pixels
|
||||
///
|
||||
/// automatically calculated by [`super::layout::ui_layout_system`]
|
||||
pub(crate) size: Vec2,
|
||||
@ -37,29 +37,34 @@ pub struct ComputedNode {
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) outline_offset: f32,
|
||||
/// The unrounded size of the node as width and height in logical pixels.
|
||||
/// The unrounded size of the node as width and height in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) unrounded_size: Vec2,
|
||||
/// Resolved border values in logical pixels
|
||||
/// Resolved border values in physical pixels
|
||||
/// Border updates bypass change detection.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) border: BorderRect,
|
||||
/// Resolved border radius values in logical pixels.
|
||||
/// Resolved border radius values in physical pixels.
|
||||
/// Border radius updates bypass change detection.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) border_radius: ResolvedBorderRadius,
|
||||
/// Resolved padding values in logical pixels
|
||||
/// Resolved padding values in physical pixels
|
||||
/// Padding updates bypass change detection.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) padding: BorderRect,
|
||||
/// Inverse scale factor for this Node.
|
||||
/// Multiply physical coordinates by the inverse scale factor to give logical coordinates.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
pub(crate) inverse_scale_factor: f32,
|
||||
}
|
||||
|
||||
impl ComputedNode {
|
||||
/// The calculated node size as width and height in logical pixels.
|
||||
/// The calculated node size as width and height in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -82,7 +87,7 @@ impl ComputedNode {
|
||||
self.stack_index
|
||||
}
|
||||
|
||||
/// The calculated node size as width and height in logical pixels before rounding.
|
||||
/// The calculated node size as width and height in physical pixels before rounding.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -90,7 +95,7 @@ impl ComputedNode {
|
||||
self.unrounded_size
|
||||
}
|
||||
|
||||
/// Returns the thickness of the UI node's outline in logical pixels.
|
||||
/// Returns the thickness of the UI node's outline in physical pixels.
|
||||
/// If this value is negative or `0.` then no outline will be rendered.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
@ -99,7 +104,7 @@ impl ComputedNode {
|
||||
self.outline_width
|
||||
}
|
||||
|
||||
/// Returns the amount of space between the outline and the edge of the node in logical pixels.
|
||||
/// Returns the amount of space between the outline and the edge of the node in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -139,7 +144,7 @@ impl ComputedNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the thickness of the node's border on each edge in logical pixels.
|
||||
/// Returns the thickness of the node's border on each edge in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -147,7 +152,7 @@ impl ComputedNode {
|
||||
self.border
|
||||
}
|
||||
|
||||
/// Returns the border radius for each of the node's corners in logical pixels.
|
||||
/// Returns the border radius for each of the node's corners in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -155,7 +160,7 @@ impl ComputedNode {
|
||||
self.border_radius
|
||||
}
|
||||
|
||||
/// Returns the inner border radius for each of the node's corners in logical pixels.
|
||||
/// Returns the inner border radius for each of the node's corners in physical pixels.
|
||||
pub fn inner_radius(&self) -> ResolvedBorderRadius {
|
||||
fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 {
|
||||
let s = 0.5 * size + offset;
|
||||
@ -177,7 +182,7 @@ impl ComputedNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the thickness of the node's padding on each edge in logical pixels.
|
||||
/// Returns the thickness of the node's padding on each edge in physical pixels.
|
||||
///
|
||||
/// Automatically calculated by [`super::layout::ui_layout_system`].
|
||||
#[inline]
|
||||
@ -185,7 +190,7 @@ impl ComputedNode {
|
||||
self.padding
|
||||
}
|
||||
|
||||
/// Returns the combined inset on each edge including both padding and border thickness in logical pixels.
|
||||
/// Returns the combined inset on each edge including both padding and border thickness in physical pixels.
|
||||
#[inline]
|
||||
pub const fn content_inset(&self) -> BorderRect {
|
||||
BorderRect {
|
||||
@ -195,6 +200,13 @@ impl ComputedNode {
|
||||
bottom: self.border.bottom + self.padding.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the inverse of the scale factor for this node.
|
||||
/// To convert from physical coordinates to logical coordinates multiply by this value.
|
||||
#[inline]
|
||||
pub const fn inverse_scale_factor(&self) -> f32 {
|
||||
self.inverse_scale_factor
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputedNode {
|
||||
@ -207,6 +219,7 @@ impl ComputedNode {
|
||||
border_radius: ResolvedBorderRadius::ZERO,
|
||||
border: BorderRect::ZERO,
|
||||
padding: BorderRect::ZERO,
|
||||
inverse_scale_factor: 1.,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2330,10 +2343,15 @@ impl BorderRadius {
|
||||
}
|
||||
|
||||
/// Compute the logical border radius for a single corner from the given values
|
||||
pub fn resolve_single_corner(radius: Val, node_size: Vec2, viewport_size: Vec2) -> f32 {
|
||||
pub fn resolve_single_corner(
|
||||
radius: Val,
|
||||
node_size: Vec2,
|
||||
viewport_size: Vec2,
|
||||
scale_factor: f32,
|
||||
) -> f32 {
|
||||
match radius {
|
||||
Val::Auto => 0.,
|
||||
Val::Px(px) => px,
|
||||
Val::Px(px) => px * scale_factor,
|
||||
Val::Percent(percent) => node_size.min_element() * percent / 100.,
|
||||
Val::Vw(percent) => viewport_size.x * percent / 100.,
|
||||
Val::Vh(percent) => viewport_size.y * percent / 100.,
|
||||
@ -2343,19 +2361,44 @@ impl BorderRadius {
|
||||
.clamp(0., 0.5 * node_size.min_element())
|
||||
}
|
||||
|
||||
pub fn resolve(&self, node_size: Vec2, viewport_size: Vec2) -> ResolvedBorderRadius {
|
||||
pub fn resolve(
|
||||
&self,
|
||||
node_size: Vec2,
|
||||
viewport_size: Vec2,
|
||||
scale_factor: f32,
|
||||
) -> ResolvedBorderRadius {
|
||||
ResolvedBorderRadius {
|
||||
top_left: Self::resolve_single_corner(self.top_left, node_size, viewport_size),
|
||||
top_right: Self::resolve_single_corner(self.top_right, node_size, viewport_size),
|
||||
bottom_left: Self::resolve_single_corner(self.bottom_left, node_size, viewport_size),
|
||||
bottom_right: Self::resolve_single_corner(self.bottom_right, node_size, viewport_size),
|
||||
top_left: Self::resolve_single_corner(
|
||||
self.top_left,
|
||||
node_size,
|
||||
viewport_size,
|
||||
scale_factor,
|
||||
),
|
||||
top_right: Self::resolve_single_corner(
|
||||
self.top_right,
|
||||
node_size,
|
||||
viewport_size,
|
||||
scale_factor,
|
||||
),
|
||||
bottom_left: Self::resolve_single_corner(
|
||||
self.bottom_left,
|
||||
node_size,
|
||||
viewport_size,
|
||||
scale_factor,
|
||||
),
|
||||
bottom_right: Self::resolve_single_corner(
|
||||
self.bottom_right,
|
||||
node_size,
|
||||
viewport_size,
|
||||
scale_factor,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the resolved border radius values for a UI node.
|
||||
///
|
||||
/// The values are in logical pixels.
|
||||
/// The values are in physical pixels.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Reflect)]
|
||||
pub struct ResolvedBorderRadius {
|
||||
pub top_left: f32,
|
||||
|
@ -115,7 +115,8 @@ fn update_clipping(
|
||||
clip_rect.max.x -= clip_inset.right;
|
||||
clip_rect.max.y -= clip_inset.bottom;
|
||||
|
||||
clip_rect = clip_rect.inflate(node.overflow_clip_margin.margin.max(0.));
|
||||
clip_rect = clip_rect
|
||||
.inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor);
|
||||
|
||||
if node.overflow.x == OverflowAxis::Visible {
|
||||
clip_rect.min.x = -f32::INFINITY;
|
||||
|
@ -350,10 +350,7 @@ fn queue_text(
|
||||
TextBounds::UNBOUNDED
|
||||
} else {
|
||||
// `scale_factor` is already multiplied by `UiScale`
|
||||
TextBounds::new(
|
||||
node.unrounded_size.x * scale_factor,
|
||||
node.unrounded_size.y * scale_factor,
|
||||
)
|
||||
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
|
||||
};
|
||||
|
||||
let text_layout_info = text_layout_info.into_inner();
|
||||
@ -398,12 +395,7 @@ fn queue_text(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn text_system(
|
||||
mut textures: ResMut<Assets<Image>>,
|
||||
mut scale_factors_buffer: Local<EntityHashMap<f32>>,
|
||||
mut last_scale_factors: Local<EntityHashMap<f32>>,
|
||||
fonts: Res<Assets<Font>>,
|
||||
camera_query: Query<(Entity, &Camera)>,
|
||||
default_ui_camera: DefaultUiCamera,
|
||||
ui_scale: Res<UiScale>,
|
||||
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
|
||||
mut font_atlas_sets: ResMut<FontAtlasSets>,
|
||||
mut text_pipeline: ResMut<TextPipeline>,
|
||||
@ -414,40 +406,13 @@ pub fn text_system(
|
||||
&mut TextLayoutInfo,
|
||||
&mut TextNodeFlags,
|
||||
&mut ComputedTextBlock,
|
||||
Option<&TargetCamera>,
|
||||
)>,
|
||||
mut text_reader: TextUiReader,
|
||||
mut font_system: ResMut<CosmicFontSystem>,
|
||||
mut swash_cache: ResMut<SwashCache>,
|
||||
) {
|
||||
scale_factors_buffer.clear();
|
||||
|
||||
for (entity, node, block, text_layout_info, text_flags, mut computed, maybe_camera) in
|
||||
&mut text_query
|
||||
{
|
||||
let Some(camera_entity) = maybe_camera
|
||||
.map(TargetCamera::entity)
|
||||
.or(default_ui_camera.get())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let scale_factor = match scale_factors_buffer.entry(camera_entity) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => *entry.insert(
|
||||
camera_query
|
||||
.get(camera_entity)
|
||||
.ok()
|
||||
.and_then(|(_, c)| c.target_scaling_factor())
|
||||
.unwrap_or(1.0)
|
||||
* ui_scale.0,
|
||||
),
|
||||
};
|
||||
let inverse_scale_factor = scale_factor.recip();
|
||||
|
||||
if last_scale_factors.get(&camera_entity) != Some(&scale_factor)
|
||||
|| node.is_changed()
|
||||
|| text_flags.needs_recompute
|
||||
{
|
||||
for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query {
|
||||
if node.is_changed() || text_flags.needs_recompute {
|
||||
queue_text(
|
||||
entity,
|
||||
&fonts,
|
||||
@ -455,8 +420,8 @@ pub fn text_system(
|
||||
&mut font_atlas_sets,
|
||||
&mut texture_atlases,
|
||||
&mut textures,
|
||||
scale_factor,
|
||||
inverse_scale_factor,
|
||||
node.inverse_scale_factor.recip(),
|
||||
node.inverse_scale_factor,
|
||||
block,
|
||||
node,
|
||||
text_flags,
|
||||
@ -468,5 +433,4 @@ pub fn text_system(
|
||||
);
|
||||
}
|
||||
}
|
||||
core::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer);
|
||||
}
|
||||
|
@ -233,13 +233,13 @@ fn update_animation(
|
||||
|
||||
fn update_transform<T: UpdateTransform + Component>(
|
||||
animation: Res<AnimationState>,
|
||||
mut containers: Query<(&mut Transform, &mut Node, &T)>,
|
||||
mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>,
|
||||
) {
|
||||
for (mut transform, mut node, update_transform) in &mut containers {
|
||||
for (mut transform, mut node, computed_node, update_transform) in &mut containers {
|
||||
update_transform.update(animation.t, &mut transform);
|
||||
|
||||
node.left = Val::Px(transform.translation.x);
|
||||
node.top = Val::Px(transform.translation.y);
|
||||
node.left = Val::Px(transform.translation.x * computed_node.inverse_scale_factor());
|
||||
node.top = Val::Px(transform.translation.y * computed_node.inverse_scale_factor());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,26 +51,24 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
for ([width, height], flip_x, flip_y) in [
|
||||
([160., 160.], false, false),
|
||||
([320., 160.], false, true),
|
||||
([320., 160.], true, false),
|
||||
([160., 160.], true, true),
|
||||
] {
|
||||
parent.spawn((
|
||||
ImageNode {
|
||||
image: image.clone(),
|
||||
flip_x,
|
||||
flip_y,
|
||||
image_mode: NodeImageMode::Sliced(slicer.clone()),
|
||||
..default()
|
||||
},
|
||||
Node {
|
||||
width: Val::Px(width),
|
||||
height: Val::Px(height),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
for [columns, rows] in [[3., 3.], [4., 4.], [5., 4.], [4., 5.], [5., 5.]] {
|
||||
for (flip_x, flip_y) in [(false, false), (false, true), (true, false), (true, true)]
|
||||
{
|
||||
parent.spawn((
|
||||
ImageNode {
|
||||
image: image.clone(),
|
||||
flip_x,
|
||||
flip_y,
|
||||
image_mode: NodeImageMode::Sliced(slicer.clone()),
|
||||
..default()
|
||||
},
|
||||
Node {
|
||||
width: Val::Px(16. * columns),
|
||||
height: Val::Px(16. * rows),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user