Path: blob/main/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs
6596 views
use core::{hash::Hash, ops::Range};12use crate::*;3use bevy_asset::*;4use bevy_color::{ColorToComponents, LinearRgba};5use bevy_ecs::{6prelude::Component,7system::{8lifetimeless::{Read, SRes},9*,10},11};12use bevy_image::prelude::*;13use bevy_math::{Affine2, FloatOrd, Rect, Vec2};14use bevy_mesh::VertexBufferLayout;15use bevy_platform::collections::HashMap;16use bevy_render::{17render_asset::RenderAssets,18render_phase::*,19render_resource::{binding_types::uniform_buffer, *},20renderer::{RenderDevice, RenderQueue},21texture::GpuImage,22view::*,23Extract, ExtractSchedule, Render, RenderSystems,24};25use bevy_render::{sync_world::MainEntity, RenderStartup};26use bevy_shader::Shader;27use bevy_sprite::{SliceScaleMode, SpriteImageMode, TextureSlicer};28use bevy_sprite_render::SpriteAssetEvents;29use bevy_ui::widget;30use bevy_utils::default;31use binding_types::{sampler, texture_2d};32use bytemuck::{Pod, Zeroable};3334pub struct UiTextureSlicerPlugin;3536impl Plugin for UiTextureSlicerPlugin {37fn build(&self, app: &mut App) {38embedded_asset!(app, "ui_texture_slice.wgsl");3940if let Some(render_app) = app.get_sub_app_mut(RenderApp) {41render_app42.add_render_command::<TransparentUi, DrawUiTextureSlices>()43.init_resource::<ExtractedUiTextureSlices>()44.init_resource::<UiTextureSliceMeta>()45.init_resource::<UiTextureSliceImageBindGroups>()46.init_resource::<SpecializedRenderPipelines<UiTextureSlicePipeline>>()47.add_systems(RenderStartup, init_ui_texture_slice_pipeline)48.add_systems(49ExtractSchedule,50extract_ui_texture_slices.in_set(RenderUiSystems::ExtractTextureSlice),51)52.add_systems(53Render,54(55queue_ui_slices.in_set(RenderSystems::Queue),56prepare_ui_slices.in_set(RenderSystems::PrepareBindGroups),57),58);59}60}61}6263#[repr(C)]64#[derive(Copy, Clone, Pod, Zeroable)]65struct UiTextureSliceVertex {66pub position: [f32; 3],67pub uv: [f32; 2],68pub color: [f32; 4],69pub slices: [f32; 4],70pub border: [f32; 4],71pub repeat: [f32; 4],72pub atlas: [f32; 4],73}7475#[derive(Component)]76pub struct UiTextureSlicerBatch {77pub range: Range<u32>,78pub image: AssetId<Image>,79}8081#[derive(Resource)]82pub struct UiTextureSliceMeta {83vertices: RawBufferVec<UiTextureSliceVertex>,84indices: RawBufferVec<u32>,85view_bind_group: Option<BindGroup>,86}8788impl Default for UiTextureSliceMeta {89fn default() -> Self {90Self {91vertices: RawBufferVec::new(BufferUsages::VERTEX),92indices: RawBufferVec::new(BufferUsages::INDEX),93view_bind_group: None,94}95}96}9798#[derive(Resource, Default)]99pub struct UiTextureSliceImageBindGroups {100pub values: HashMap<AssetId<Image>, BindGroup>,101}102103#[derive(Resource)]104pub struct UiTextureSlicePipeline {105pub view_layout: BindGroupLayout,106pub image_layout: BindGroupLayout,107pub shader: Handle<Shader>,108}109110pub fn init_ui_texture_slice_pipeline(111mut commands: Commands,112render_device: Res<RenderDevice>,113asset_server: Res<AssetServer>,114) {115let view_layout = render_device.create_bind_group_layout(116"ui_texture_slice_view_layout",117&BindGroupLayoutEntries::single(118ShaderStages::VERTEX_FRAGMENT,119uniform_buffer::<ViewUniform>(true),120),121);122123let image_layout = render_device.create_bind_group_layout(124"ui_texture_slice_image_layout",125&BindGroupLayoutEntries::sequential(126ShaderStages::FRAGMENT,127(128texture_2d(TextureSampleType::Float { filterable: true }),129sampler(SamplerBindingType::Filtering),130),131),132);133134commands.insert_resource(UiTextureSlicePipeline {135view_layout,136image_layout,137shader: load_embedded_asset!(asset_server.as_ref(), "ui_texture_slice.wgsl"),138});139}140141#[derive(Clone, Copy, Hash, PartialEq, Eq)]142pub struct UiTextureSlicePipelineKey {143pub hdr: bool,144}145146impl SpecializedRenderPipeline for UiTextureSlicePipeline {147type Key = UiTextureSlicePipelineKey;148149fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {150let vertex_layout = VertexBufferLayout::from_vertex_formats(151VertexStepMode::Vertex,152vec![153// position154VertexFormat::Float32x3,155// uv156VertexFormat::Float32x2,157// color158VertexFormat::Float32x4,159// normalized texture slicing lines (left, top, right, bottom)160VertexFormat::Float32x4,161// normalized target slicing lines (left, top, right, bottom)162VertexFormat::Float32x4,163// repeat values (horizontal side, vertical side, horizontal center, vertical center)164VertexFormat::Float32x4,165// normalized texture atlas rect (left, top, right, bottom)166VertexFormat::Float32x4,167],168);169let shader_defs = Vec::new();170171RenderPipelineDescriptor {172vertex: VertexState {173shader: self.shader.clone(),174shader_defs: shader_defs.clone(),175buffers: vec![vertex_layout],176..default()177},178fragment: Some(FragmentState {179shader: self.shader.clone(),180shader_defs,181targets: vec![Some(ColorTargetState {182format: if key.hdr {183ViewTarget::TEXTURE_FORMAT_HDR184} else {185TextureFormat::bevy_default()186},187blend: Some(BlendState::ALPHA_BLENDING),188write_mask: ColorWrites::ALL,189})],190..default()191}),192layout: vec![self.view_layout.clone(), self.image_layout.clone()],193label: Some("ui_texture_slice_pipeline".into()),194..default()195}196}197}198199pub struct ExtractedUiTextureSlice {200pub stack_index: u32,201pub transform: Affine2,202pub rect: Rect,203pub atlas_rect: Option<Rect>,204pub image: AssetId<Image>,205pub clip: Option<Rect>,206pub extracted_camera_entity: Entity,207pub color: LinearRgba,208pub image_scale_mode: SpriteImageMode,209pub flip_x: bool,210pub flip_y: bool,211pub inverse_scale_factor: f32,212pub main_entity: MainEntity,213pub render_entity: Entity,214}215216#[derive(Resource, Default)]217pub struct ExtractedUiTextureSlices {218pub slices: Vec<ExtractedUiTextureSlice>,219}220221pub fn extract_ui_texture_slices(222mut commands: Commands,223mut extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,224texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,225slicers_query: Extract<226Query<(227Entity,228&ComputedNode,229&UiGlobalTransform,230&InheritedVisibility,231Option<&CalculatedClip>,232&ComputedUiTargetCamera,233&ImageNode,234)>,235>,236camera_map: Extract<UiCameraMap>,237) {238let mut camera_mapper = camera_map.get_mapper();239240for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &slicers_query {241// Skip invisible images242if !inherited_visibility.get()243|| image.color.is_fully_transparent()244|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()245{246continue;247}248249let image_scale_mode = match image.image_mode.clone() {250widget::NodeImageMode::Sliced(texture_slicer) => {251SpriteImageMode::Sliced(texture_slicer)252}253widget::NodeImageMode::Tiled {254tile_x,255tile_y,256stretch_value,257} => SpriteImageMode::Tiled {258tile_x,259tile_y,260stretch_value,261},262_ => continue,263};264265let Some(extracted_camera_entity) = camera_mapper.map(camera) else {266continue;267};268269let atlas_rect = image270.texture_atlas271.as_ref()272.and_then(|s| s.texture_rect(&texture_atlases))273.map(|r| r.as_rect());274275let atlas_rect = match (atlas_rect, image.rect) {276(None, None) => None,277(None, Some(image_rect)) => Some(image_rect),278(Some(atlas_rect), None) => Some(atlas_rect),279(Some(atlas_rect), Some(mut image_rect)) => {280image_rect.min += atlas_rect.min;281image_rect.max += atlas_rect.min;282Some(image_rect)283}284};285286extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {287render_entity: commands.spawn(TemporaryRenderEntity).id(),288stack_index: uinode.stack_index,289transform: transform.into(),290color: image.color.into(),291rect: Rect {292min: Vec2::ZERO,293max: uinode.size,294},295clip: clip.map(|clip| clip.clip),296image: image.image.id(),297extracted_camera_entity,298image_scale_mode,299atlas_rect,300flip_x: image.flip_x,301flip_y: image.flip_y,302inverse_scale_factor: uinode.inverse_scale_factor,303main_entity: entity.into(),304});305}306}307308#[expect(309clippy::too_many_arguments,310reason = "it's a system that needs a lot of them"311)]312pub fn queue_ui_slices(313extracted_ui_slicers: ResMut<ExtractedUiTextureSlices>,314ui_slicer_pipeline: Res<UiTextureSlicePipeline>,315mut pipelines: ResMut<SpecializedRenderPipelines<UiTextureSlicePipeline>>,316mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,317mut render_views: Query<&UiCameraView, With<ExtractedView>>,318camera_views: Query<&ExtractedView>,319pipeline_cache: Res<PipelineCache>,320draw_functions: Res<DrawFunctions<TransparentUi>>,321) {322let draw_function = draw_functions.read().id::<DrawUiTextureSlices>();323for (index, extracted_slicer) in extracted_ui_slicers.slices.iter().enumerate() {324let Ok(default_camera_view) =325render_views.get_mut(extracted_slicer.extracted_camera_entity)326else {327continue;328};329330let Ok(view) = camera_views.get(default_camera_view.0) else {331continue;332};333334let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)335else {336continue;337};338339let pipeline = pipelines.specialize(340&pipeline_cache,341&ui_slicer_pipeline,342UiTextureSlicePipelineKey { hdr: view.hdr },343);344345transparent_phase.add(TransparentUi {346draw_function,347pipeline,348entity: (extracted_slicer.render_entity, extracted_slicer.main_entity),349sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE),350batch_range: 0..0,351extra_index: PhaseItemExtraIndex::None,352index,353indexed: true,354});355}356}357358pub fn prepare_ui_slices(359mut commands: Commands,360render_device: Res<RenderDevice>,361render_queue: Res<RenderQueue>,362mut ui_meta: ResMut<UiTextureSliceMeta>,363mut extracted_slices: ResMut<ExtractedUiTextureSlices>,364view_uniforms: Res<ViewUniforms>,365texture_slicer_pipeline: Res<UiTextureSlicePipeline>,366mut image_bind_groups: ResMut<UiTextureSliceImageBindGroups>,367gpu_images: Res<RenderAssets<GpuImage>>,368mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,369events: Res<SpriteAssetEvents>,370mut previous_len: Local<usize>,371) {372// If an image has changed, the GpuImage has (probably) changed373for event in &events.images {374match event {375AssetEvent::Added { .. } |376AssetEvent::Unused { .. } |377// Images don't have dependencies378AssetEvent::LoadedWithDependencies { .. } => {}379AssetEvent::Modified { id } | AssetEvent::Removed { id } => {380image_bind_groups.values.remove(id);381}382};383}384385if let Some(view_binding) = view_uniforms.uniforms.binding() {386let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len);387388ui_meta.vertices.clear();389ui_meta.indices.clear();390ui_meta.view_bind_group = Some(render_device.create_bind_group(391"ui_texture_slice_view_bind_group",392&texture_slicer_pipeline.view_layout,393&BindGroupEntries::single(view_binding),394));395396// Buffer indexes397let mut vertices_index = 0;398let mut indices_index = 0;399400for ui_phase in phases.values_mut() {401let mut batch_item_index = 0;402let mut batch_image_handle = AssetId::invalid();403let mut batch_image_size = Vec2::ZERO;404405for item_index in 0..ui_phase.items.len() {406let item = &mut ui_phase.items[item_index];407if let Some(texture_slices) = extracted_slices408.slices409.get(item.index)410.filter(|n| item.entity() == n.render_entity)411{412let mut existing_batch = batches.last_mut();413414if batch_image_handle == AssetId::invalid()415|| existing_batch.is_none()416|| (batch_image_handle != AssetId::default()417&& texture_slices.image != AssetId::default()418&& batch_image_handle != texture_slices.image)419{420if let Some(gpu_image) = gpu_images.get(texture_slices.image) {421batch_item_index = item_index;422batch_image_handle = texture_slices.image;423batch_image_size = gpu_image.size_2d().as_vec2();424425let new_batch = UiTextureSlicerBatch {426range: vertices_index..vertices_index,427image: texture_slices.image,428};429430batches.push((item.entity(), new_batch));431432image_bind_groups433.values434.entry(batch_image_handle)435.or_insert_with(|| {436render_device.create_bind_group(437"ui_texture_slice_image_layout",438&texture_slicer_pipeline.image_layout,439&BindGroupEntries::sequential((440&gpu_image.texture_view,441&gpu_image.sampler,442)),443)444});445446existing_batch = batches.last_mut();447} else {448continue;449}450} else if let Some(ref mut existing_batch) = existing_batch451&& batch_image_handle == AssetId::default()452&& texture_slices.image != AssetId::default()453{454if let Some(gpu_image) = gpu_images.get(texture_slices.image) {455batch_image_handle = texture_slices.image;456batch_image_size = gpu_image.size_2d().as_vec2();457existing_batch.1.image = texture_slices.image;458459image_bind_groups460.values461.entry(batch_image_handle)462.or_insert_with(|| {463render_device.create_bind_group(464"ui_texture_slice_image_layout",465&texture_slicer_pipeline.image_layout,466&BindGroupEntries::sequential((467&gpu_image.texture_view,468&gpu_image.sampler,469)),470)471});472} else {473continue;474}475}476477let uinode_rect = texture_slices.rect;478479let rect_size = uinode_rect.size();480481// Specify the corners of the node482let positions = QUAD_VERTEX_POSITIONS.map(|pos| {483(texture_slices.transform.transform_point2(pos * rect_size)).extend(0.)484});485486// Calculate the effect of clipping487// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)488let positions_diff = if let Some(clip) = texture_slices.clip {489[490Vec2::new(491f32::max(clip.min.x - positions[0].x, 0.),492f32::max(clip.min.y - positions[0].y, 0.),493),494Vec2::new(495f32::min(clip.max.x - positions[1].x, 0.),496f32::max(clip.min.y - positions[1].y, 0.),497),498Vec2::new(499f32::min(clip.max.x - positions[2].x, 0.),500f32::min(clip.max.y - positions[2].y, 0.),501),502Vec2::new(503f32::max(clip.min.x - positions[3].x, 0.),504f32::min(clip.max.y - positions[3].y, 0.),505),506]507} else {508[Vec2::ZERO; 4]509};510511let positions_clipped = [512positions[0] + positions_diff[0].extend(0.),513positions[1] + positions_diff[1].extend(0.),514positions[2] + positions_diff[2].extend(0.),515positions[3] + positions_diff[3].extend(0.),516];517518let transformed_rect_size =519texture_slices.transform.transform_vector2(rect_size);520521// Don't try to cull nodes that have a rotation522// In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π523// In those two cases, the culling check can proceed normally as corners will be on524// horizontal / vertical lines525// For all other angles, bypass the culling check526// This does not properly handles all rotations on all axis527if texture_slices.transform.x_axis[1] == 0.0 {528// Cull nodes that are completely clipped529if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x530|| positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y531{532continue;533}534}535let flags = if texture_slices.image != AssetId::default() {536shader_flags::TEXTURED537} else {538shader_flags::UNTEXTURED539};540541let uvs = if flags == shader_flags::UNTEXTURED {542[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]543} else {544let atlas_extent = uinode_rect.max;545[546Vec2::new(547uinode_rect.min.x + positions_diff[0].x,548uinode_rect.min.y + positions_diff[0].y,549),550Vec2::new(551uinode_rect.max.x + positions_diff[1].x,552uinode_rect.min.y + positions_diff[1].y,553),554Vec2::new(555uinode_rect.max.x + positions_diff[2].x,556uinode_rect.max.y + positions_diff[2].y,557),558Vec2::new(559uinode_rect.min.x + positions_diff[3].x,560uinode_rect.max.y + positions_diff[3].y,561),562]563.map(|pos| pos / atlas_extent)564};565566let color = texture_slices.color.to_f32_array();567568let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect {569(570atlas.size(),571[572atlas.min.x / batch_image_size.x,573atlas.min.y / batch_image_size.y,574atlas.max.x / batch_image_size.x,575atlas.max.y / batch_image_size.y,576],577)578} else {579(batch_image_size, [0., 0., 1., 1.])580};581582if texture_slices.flip_x {583atlas.swap(0, 2);584}585586if texture_slices.flip_y {587atlas.swap(1, 3);588}589590let [slices, border, repeat] = compute_texture_slices(591image_size,592uinode_rect.size() * texture_slices.inverse_scale_factor,593&texture_slices.image_scale_mode,594);595596for i in 0..4 {597ui_meta.vertices.push(UiTextureSliceVertex {598position: positions_clipped[i].into(),599uv: uvs[i].into(),600color,601slices,602border,603repeat,604atlas,605});606}607608for &i in &QUAD_INDICES {609ui_meta.indices.push(indices_index + i as u32);610}611612vertices_index += 6;613indices_index += 4;614615existing_batch.unwrap().1.range.end = vertices_index;616ui_phase.items[batch_item_index].batch_range_mut().end += 1;617} else {618batch_image_handle = AssetId::invalid();619}620}621}622ui_meta.vertices.write_buffer(&render_device, &render_queue);623ui_meta.indices.write_buffer(&render_device, &render_queue);624*previous_len = batches.len();625commands.try_insert_batch(batches);626}627extracted_slices.slices.clear();628}629630pub type DrawUiTextureSlices = (631SetItemPipeline,632SetSlicerViewBindGroup<0>,633SetSlicerTextureBindGroup<1>,634DrawSlicer,635);636637pub struct SetSlicerViewBindGroup<const I: usize>;638impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerViewBindGroup<I> {639type Param = SRes<UiTextureSliceMeta>;640type ViewQuery = Read<ViewUniformOffset>;641type ItemQuery = ();642643fn render<'w>(644_item: &P,645view_uniform: &'w ViewUniformOffset,646_entity: Option<()>,647ui_meta: SystemParamItem<'w, '_, Self::Param>,648pass: &mut TrackedRenderPass<'w>,649) -> RenderCommandResult {650let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else {651return RenderCommandResult::Failure("view_bind_group not available");652};653pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]);654RenderCommandResult::Success655}656}657pub struct SetSlicerTextureBindGroup<const I: usize>;658impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetSlicerTextureBindGroup<I> {659type Param = SRes<UiTextureSliceImageBindGroups>;660type ViewQuery = ();661type ItemQuery = Read<UiTextureSlicerBatch>;662663#[inline]664fn render<'w>(665_item: &P,666_view: (),667batch: Option<&'w UiTextureSlicerBatch>,668image_bind_groups: SystemParamItem<'w, '_, Self::Param>,669pass: &mut TrackedRenderPass<'w>,670) -> RenderCommandResult {671let image_bind_groups = image_bind_groups.into_inner();672let Some(batch) = batch else {673return RenderCommandResult::Skip;674};675676pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]);677RenderCommandResult::Success678}679}680pub struct DrawSlicer;681impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {682type Param = SRes<UiTextureSliceMeta>;683type ViewQuery = ();684type ItemQuery = Read<UiTextureSlicerBatch>;685686#[inline]687fn render<'w>(688_item: &P,689_view: (),690batch: Option<&'w UiTextureSlicerBatch>,691ui_meta: SystemParamItem<'w, '_, Self::Param>,692pass: &mut TrackedRenderPass<'w>,693) -> RenderCommandResult {694let Some(batch) = batch else {695return RenderCommandResult::Skip;696};697let ui_meta = ui_meta.into_inner();698let Some(vertices) = ui_meta.vertices.buffer() else {699return RenderCommandResult::Failure("missing vertices to draw ui");700};701let Some(indices) = ui_meta.indices.buffer() else {702return RenderCommandResult::Failure("missing indices to draw ui");703};704705// Store the vertices706pass.set_vertex_buffer(0, vertices.slice(..));707// Define how to "connect" the vertices708pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32);709// Draw the vertices710pass.draw_indexed(batch.range.clone(), 0, 0..1);711RenderCommandResult::Success712}713}714715fn compute_texture_slices(716image_size: Vec2,717target_size: Vec2,718image_scale_mode: &SpriteImageMode,719) -> [[f32; 4]; 3] {720match image_scale_mode {721SpriteImageMode::Sliced(TextureSlicer {722border: border_rect,723center_scale_mode,724sides_scale_mode,725max_corner_scale,726}) => {727let min_coeff = (target_size / image_size)728.min_element()729.min(*max_corner_scale);730731// calculate the normalized extents of the nine-patched image slices732let slices = [733border_rect.left / image_size.x,734border_rect.top / image_size.y,7351. - border_rect.right / image_size.x,7361. - border_rect.bottom / image_size.y,737];738739// calculate the normalized extents of the target slices740let border = [741(border_rect.left / target_size.x) * min_coeff,742(border_rect.top / target_size.y) * min_coeff,7431. - (border_rect.right / target_size.x) * min_coeff,7441. - (border_rect.bottom / target_size.y) * min_coeff,745];746747let image_side_width = image_size.x * (slices[2] - slices[0]);748let image_side_height = image_size.y * (slices[3] - slices[1]);749let target_side_width = target_size.x * (border[2] - border[0]);750let target_side_height = target_size.y * (border[3] - border[1]);751752// compute the number of times to repeat the side and center slices when tiling along each axis753// if the returned value is `1.` the slice will be stretched to fill the axis.754let repeat_side_x =755compute_tiled_subaxis(image_side_width, target_side_width, sides_scale_mode);756let repeat_side_y =757compute_tiled_subaxis(image_side_height, target_side_height, sides_scale_mode);758let repeat_center_x =759compute_tiled_subaxis(image_side_width, target_side_width, center_scale_mode);760let repeat_center_y =761compute_tiled_subaxis(image_side_height, target_side_height, center_scale_mode);762763[764slices,765border,766[767repeat_side_x,768repeat_side_y,769repeat_center_x,770repeat_center_y,771],772]773}774SpriteImageMode::Tiled {775tile_x,776tile_y,777stretch_value,778} => {779let rx = compute_tiled_axis(*tile_x, image_size.x, target_size.x, *stretch_value);780let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);781[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]782}783SpriteImageMode::Auto => {784unreachable!("Slices can not be computed for SpriteImageMode::Stretch")785}786SpriteImageMode::Scale(_) => {787unreachable!("Slices can not be computed for SpriteImageMode::Scale")788}789}790}791792fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 {793if tile {794let s = image_extent * stretch;795target_extent / s796} else {7971.798}799}800801fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 {802match mode {803SliceScaleMode::Stretch => 1.,804SliceScaleMode::Tile { stretch_value } => {805let s = image_extent * *stretch_value;806target_extent / s807}808}809}810811812