use bevy::camera::visibility::RenderLayers;
use bevy::camera::{ClearColorConfig, RenderTarget};
use bevy::image::{TextureAtlas, TextureAtlasLayout};
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages};
use bevy::ui::ComputedNode;
use bevy::ui::widget::{ImageNode, NodeImageMode};
use bevy_pf_vector::{
Brush, DashPattern, FillRule as VFillRule, GradientStop, HudTransform, LineCap, LineJoin,
PathCommand, PathStyle, StrokeStyle, VectorPrimitive, VectorShape,
};
use bevy_pf_xaml::geometry::{FillRule, PathData, PathSegment};
use bevy_pf_xaml::value as v;
use crate::shapes::{PfShape, PfShapeClaim, PfShapeRendered, ShapeGeometry, arc_to_cubics};
const SHAPE_LAYER: usize = 24;
const ATLAS_SIZE: u32 = 2048;
const SLOT_PADDING: u32 = 2;
#[derive(Resource)]
pub struct PfShapeAtlas {
pub image: Handle<Image>,
pub layout: Handle<TextureAtlasLayout>,
cursor: UVec2,
shelf_height: u32,
free: std::collections::HashMap<UVec2, Vec<UVec2>>,
}
impl PfShapeAtlas {
fn allocate(&mut self, size: UVec2) -> Option<(UVec2, UVec2)> {
if let Some(origins) = self.free.get_mut(&size)
&& let Some(origin) = origins.pop()
{
return Some((origin, size));
}
let best = self
.free
.iter()
.filter(|(cap, origins)| !origins.is_empty() && cap.x >= size.x && cap.y >= size.y)
.min_by_key(|(cap, _)| cap.x as u64 * cap.y as u64)
.map(|(cap, _)| *cap);
if let Some(cap) = best
&& let Some(origins) = self.free.get_mut(&cap)
&& let Some(origin) = origins.pop()
{
return Some((origin, cap));
}
let step = size + UVec2::splat(SLOT_PADDING);
if step.x > ATLAS_SIZE || step.y > ATLAS_SIZE {
return None;
}
if self.cursor.x + step.x > ATLAS_SIZE {
self.cursor.x = 0;
self.cursor.y += self.shelf_height;
self.shelf_height = 0;
}
if self.cursor.y + step.y > ATLAS_SIZE {
return None;
}
let origin = self.cursor;
self.cursor.x += step.x;
self.shelf_height = self.shelf_height.max(step.y);
Some((origin, size))
}
fn release(&mut self, capacity: UVec2, origin: UVec2) {
self.free.entry(capacity).or_default().push(origin);
}
fn reset(&mut self) {
self.cursor = UVec2::ZERO;
self.shelf_height = 0;
self.free.clear();
}
}
#[derive(Component, Debug, Clone)]
#[component(on_remove = release_slot)]
pub struct PfShapeGpu {
index: usize,
origin: UVec2,
capacity: UVec2,
size: UVec2,
draw: Entity,
draw_stroke: Option<Entity>,
}
fn release_slot(
mut world: bevy::ecs::world::DeferredWorld,
ctx: bevy::ecs::lifecycle::HookContext,
) {
let Some(gpu) = world.get::<PfShapeGpu>(ctx.entity).cloned() else {
return;
};
if let Some(mut atlas) = world.get_resource_mut::<PfShapeAtlas>() {
atlas.release(gpu.capacity, gpu.origin);
}
let mut commands = world.commands();
commands.entity(gpu.draw).try_despawn();
if let Some(stroke) = gpu.draw_stroke {
commands.entity(stroke).try_despawn();
}
}
fn slot_capacity(px: UVec2) -> UVec2 {
const GRAIN: u32 = 16;
UVec2::new(px.x.div_ceil(GRAIN) * GRAIN, px.y.div_ceil(GRAIN) * GRAIN)
}
#[derive(Resource, Default)]
struct PfAtlasFull(bool);
#[derive(Resource, Default)]
pub struct PfAtlasRebuilds(pub u32);
#[derive(Component)]
struct PfShapeAtlasCamera;
#[derive(Resource, Default)]
struct PfAtlasDirty(u8);
pub struct PfShapeGpuPlugin;
impl Plugin for PfShapeGpuPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<bevy_pf_vector::PfVectorPlugin>() {
app.add_plugins(bevy_pf_vector::PfVectorPlugin);
}
let claim = (sync_gpu_shapes, rebuild_atlas_if_full, gate_atlas_camera)
.chain()
.in_set(crate::shapes::PfShapeSystems::Claim);
#[cfg(feature = "native_shapes")]
let claim = claim.after(crate::shapes::style_native_shapes);
app.init_resource::<PfAtlasFull>()
.init_resource::<PfAtlasRebuilds>()
.init_resource::<PfAtlasDirty>()
.add_systems(Startup, setup_atlas)
.add_systems(PostUpdate, claim);
}
}
fn setup_atlas(
mut commands: Commands,
images: Option<ResMut<Assets<Image>>>,
layouts: Option<ResMut<Assets<TextureAtlasLayout>>>,
) {
let (Some(mut images), Some(mut layouts)) = (images, layouts) else {
return;
};
let mut image = Image::new_fill(
Extent3d {
width: ATLAS_SIZE,
height: ATLAS_SIZE,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8UnormSrgb,
bevy::asset::RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage =
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
let image = images.add(image);
let layout = layouts.add(TextureAtlasLayout::new_empty(UVec2::splat(ATLAS_SIZE)));
let mut projection = OrthographicProjection::default_2d();
projection.scaling_mode = bevy::camera::ScalingMode::Fixed {
width: ATLAS_SIZE as f32,
height: ATLAS_SIZE as f32,
};
commands.spawn((
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(Color::NONE),
order: -100,
..default()
},
RenderTarget::Image(image.clone().into()),
Projection::Orthographic(projection),
bevy::render::view::Msaa::Off,
RenderLayers::layer(SHAPE_LAYER),
PfShapeAtlasCamera,
Name::new("PfShapeAtlasCamera"),
));
commands.insert_resource(PfShapeAtlas {
image,
layout,
cursor: UVec2::ZERO,
shelf_height: 0,
free: Default::default(),
});
}
fn slot_center_world(origin: UVec2, size: UVec2) -> Vec2 {
let half = ATLAS_SIZE as f32 * 0.5;
Vec2::new(
origin.x as f32 + size.x as f32 * 0.5 - half,
half - (origin.y as f32 + size.y as f32 * 0.5),
)
}
fn to_local(x: f32, y: f32, size: Vec2) -> Vec2 {
Vec2::new(x - size.x * 0.5, size.y * 0.5 - y)
}
fn to_color(c: v::PfColor) -> LinearRgba {
Color::srgba_u8(c.r, c.g, c.b, c.a).to_linear()
}
fn to_brush(brush: &v::PfBrush, size: Vec2) -> Brush {
let stops = |stops: &Vec<v::GradientStop>| -> Vec<GradientStop> {
stops
.iter()
.map(|s| GradientStop {
offset: s.offset.clamp(0.0, 1.0),
color: to_color(s.color),
})
.collect()
};
match brush {
v::PfBrush::Solid(c) => Brush::Solid(to_color(*c)),
v::PfBrush::LinearGradient {
start,
end,
stops: s,
} => Brush::Linear {
start: to_local(start.x * size.x, start.y * size.y, size),
end: to_local(end.x * size.x, end.y * size.y, size),
stops: stops(s),
},
v::PfBrush::RadialGradient {
center,
radius_x,
radius_y,
stops: s,
} => Brush::Radial {
center: to_local(center.x * size.x, center.y * size.y, size),
radius: (radius_x * size.x).max(radius_y * size.y).max(1.0),
stops: stops(s),
},
}
}
fn rounded_rect(l: f32, t: f32, r: f32, b: f32, rx: f32, ry: f32, size: Vec2) -> Vec<PathCommand> {
let k = 0.5522848;
let p = |x: f32, y: f32| to_local(x, y, size);
vec![
PathCommand::MoveTo(p(l + rx, t)),
PathCommand::LineTo(p(r - rx, t)),
PathCommand::CubicTo {
ctrl1: p(r - rx + k * rx, t),
ctrl2: p(r, t + ry - k * ry),
to: p(r, t + ry),
},
PathCommand::LineTo(p(r, b - ry)),
PathCommand::CubicTo {
ctrl1: p(r, b - ry + k * ry),
ctrl2: p(r - rx + k * rx, b),
to: p(r - rx, b),
},
PathCommand::LineTo(p(l + rx, b)),
PathCommand::CubicTo {
ctrl1: p(l + rx - k * rx, b),
ctrl2: p(l, b - ry + k * ry),
to: p(l, b - ry),
},
PathCommand::LineTo(p(l, t + ry)),
PathCommand::CubicTo {
ctrl1: p(l, t + ry - k * ry),
ctrl2: p(l + rx - k * rx, t),
to: p(l + rx, t),
},
PathCommand::Close,
]
}
fn path_data_commands(data: &PathData, size: Vec2) -> Vec<PathCommand> {
let p = |pt: v::Point| to_local(pt.x, pt.y, size);
let mut out = Vec::new();
for figure in &data.figures {
out.push(PathCommand::MoveTo(p(figure.start)));
let mut cursor = figure.start;
for segment in &figure.segments {
match segment {
PathSegment::Line(to) => {
out.push(PathCommand::LineTo(p(*to)));
cursor = *to;
}
PathSegment::Cubic(c1, c2, to) => {
out.push(PathCommand::CubicTo {
ctrl1: p(*c1),
ctrl2: p(*c2),
to: p(*to),
});
cursor = *to;
}
PathSegment::Quadratic(c, to) => {
out.push(PathCommand::QuadTo {
ctrl: p(*c),
to: p(*to),
});
cursor = *to;
}
PathSegment::Arc {
radii,
rotation,
large_arc,
sweep,
to,
} => {
for (c1, c2, end) in
arc_to_cubics(cursor, *radii, *rotation, *large_arc, *sweep, *to)
{
out.push(PathCommand::CubicTo {
ctrl1: p(c1),
ctrl2: p(c2),
to: p(end),
});
}
cursor = *to;
}
}
}
if figure.closed {
out.push(PathCommand::Close);
}
}
out
}
fn commands_bounds(commands: &[PathCommand]) -> Option<(Vec2, Vec2)> {
let mut min = Vec2::splat(f32::INFINITY);
let mut max = Vec2::splat(f32::NEG_INFINITY);
let mut any = false;
let mut visit = |p: Vec2| {
min = min.min(p);
max = max.max(p);
any = true;
};
for command in commands {
match command {
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => visit(*p),
PathCommand::QuadTo { ctrl, to } => {
visit(*ctrl);
visit(*to);
}
PathCommand::CubicTo { ctrl1, ctrl2, to } => {
visit(*ctrl1);
visit(*ctrl2);
visit(*to);
}
PathCommand::Close => {}
}
}
any.then_some((min, max))
}
fn map_commands(commands: &mut [PathCommand], f: impl Fn(Vec2) -> Vec2) {
for command in commands {
match command {
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => *p = f(*p),
PathCommand::QuadTo { ctrl, to } => {
*ctrl = f(*ctrl);
*to = f(*to);
}
PathCommand::CubicTo { ctrl1, ctrl2, to } => {
*ctrl1 = f(*ctrl1);
*ctrl2 = f(*ctrl2);
*to = f(*to);
}
PathCommand::Close => {}
}
}
}
pub fn shape_to_vector(shape: &PfShape, px: UVec2) -> Option<(Vec<PathCommand>, PathStyle)> {
let size = Vec2::new(px.x as f32, px.y as f32);
let (w, h) = (size.x, size.y);
let st = shape.stroke_thickness;
let inset = if shape.stroke.is_some() {
st * 0.5
} else {
0.0
};
let (mut commands, rule) = match &shape.geometry {
ShapeGeometry::Rectangle { radius_x, radius_y } => {
let (l, t) = (inset, inset);
let (r, b) = ((w - inset).max(inset + 0.1), (h - inset).max(inset + 0.1));
let commands = if *radius_x > 0.0 || *radius_y > 0.0 {
let rx = radius_x.min((r - l) / 2.0);
let ry = radius_y.max(0.0).min((b - t) / 2.0);
rounded_rect(l, t, r, b, rx, ry, size)
} else {
let p = |x: f32, y: f32| to_local(x, y, size);
vec![
PathCommand::MoveTo(p(l, t)),
PathCommand::LineTo(p(r, t)),
PathCommand::LineTo(p(r, b)),
PathCommand::LineTo(p(l, b)),
PathCommand::Close,
]
};
(commands, VFillRule::NonZero)
}
ShapeGeometry::Ellipse => {
let (l, t) = (inset, inset);
let (r, b) = ((w - inset).max(inset + 0.1), (h - inset).max(inset + 0.1));
let (rx, ry) = ((r - l) * 0.5, (b - t) * 0.5);
(rounded_rect(l, t, r, b, rx, ry, size), VFillRule::NonZero)
}
ShapeGeometry::Line { x1, y1, x2, y2 } => (
vec![
PathCommand::MoveTo(to_local(*x1, *y1, size)),
PathCommand::LineTo(to_local(*x2, *y2, size)),
],
VFillRule::NonZero,
),
ShapeGeometry::Polyline { points, closed } => {
let mut iter = points.iter();
let first = iter.next()?;
let mut commands = vec![PathCommand::MoveTo(to_local(first.x, first.y, size))];
for p in iter {
commands.push(PathCommand::LineTo(to_local(p.x, p.y, size)));
}
if *closed {
commands.push(PathCommand::Close);
}
let rule = match shape.fill_rule {
Some(FillRule::NonZero) => VFillRule::NonZero,
_ => VFillRule::EvenOdd, };
(commands, rule)
}
ShapeGeometry::Path(data) => {
let rule = match data.fill_rule {
FillRule::EvenOdd => VFillRule::EvenOdd,
FillRule::NonZero => VFillRule::NonZero,
};
(path_data_commands(data, size), rule)
}
};
let stretchable = !matches!(
shape.geometry,
ShapeGeometry::Rectangle { .. } | ShapeGeometry::Ellipse
);
if stretchable
&& shape.stretch != v::Stretch::None
&& let Some((min, max)) = commands_bounds(&commands)
{
let extent = (max - min).max(Vec2::splat(1e-3));
let avail = Vec2::new((w - st).max(1.0), (h - st).max(1.0));
let mut scale = avail / extent;
match shape.stretch {
v::Stretch::Uniform => scale = Vec2::splat(scale.x.min(scale.y)),
v::Stretch::UniformToFill => scale = Vec2::splat(scale.x.max(scale.y)),
_ => {}
}
let centre = (min + max) * 0.5;
let target = Vec2::new(0.0, 0.0);
map_commands(&mut commands, |p| (p - centre) * scale + target);
}
let fill = shape.fill.as_ref().map(|b| to_brush(b, size));
let stroke = shape.stroke.as_ref().map(|b| {
let width = st.max(0.01);
StrokeStyle {
brush: to_brush(b, size),
width,
join: match shape.stroke_join {
v::PenLineJoin::Miter => LineJoin::Miter,
v::PenLineJoin::Bevel => LineJoin::Bevel,
v::PenLineJoin::Round => LineJoin::Round,
},
cap: match shape.stroke_cap {
v::PenLineCap::Flat => LineCap::Butt,
v::PenLineCap::Square | v::PenLineCap::Triangle => LineCap::Square,
v::PenLineCap::Round => LineCap::Round,
},
miter_limit: shape.stroke_miter_limit.max(1.0),
dash: (!shape.stroke_dash_array.is_empty()).then(|| {
let mut pattern: Vec<f32> = shape
.stroke_dash_array
.iter()
.map(|d| (d * width).max(0.01))
.collect();
if pattern.len() % 2 != 0 {
let copy = pattern.clone();
pattern.extend(copy); }
DashPattern {
pattern,
offset: shape.stroke_dash_offset * width,
}
}),
}
});
if fill.is_none() && stroke.is_none() {
return None;
}
Some((
commands,
PathStyle {
fill,
stroke,
fill_rule: rule,
},
))
}
fn as_rounded_box(shape: &PfShape, px: UVec2) -> Option<(Vec2, f32)> {
let size = Vec2::new(px.x as f32, px.y as f32);
match &shape.geometry {
ShapeGeometry::Rectangle { radius_x, radius_y } => {
if (radius_x - radius_y).abs() > 0.01 {
return None;
}
Some((size, *radius_x))
}
ShapeGeometry::Ellipse => {
if (size.x - size.y).abs() > 0.01 {
return None;
}
Some((size, size.x * 0.5))
}
_ => None,
}
}
fn solid(brush: &v::PfBrush) -> Option<Color> {
match brush {
v::PfBrush::Solid(c) => Some(Color::srgba_u8(c.r, c.g, c.b, c.a)),
_ => None,
}
}
fn spawn_draws(
commands: &mut Commands,
shape: &PfShape,
px: UVec2,
origin: UVec2,
) -> (Entity, Option<Entity>) {
let centre = slot_center_world(origin, px).extend(0.0);
let transform = || HudTransform {
translation: centre,
..default()
};
if let Some((size, radius)) = as_rounded_box(shape, px) {
let fill = shape.fill.as_ref().and_then(solid);
let stroke = shape.stroke.as_ref().and_then(solid);
if fill.is_some() || stroke.is_some() {
let st = shape.stroke_thickness;
let inset = if stroke.is_some() { st * 0.5 } else { 0.0 };
let inner = (size - Vec2::splat(inset * 2.0)).max(Vec2::splat(0.1));
let inner_radius = (radius - inset).max(0.0);
let first = commands
.spawn((
VectorPrimitive::Rect {
size: inner,
radius: inner_radius,
thickness: if fill.is_some() { 0.0 } else { st.max(0.01) },
color: fill.or(stroke).unwrap().to_linear(),
},
transform(),
RenderLayers::layer(SHAPE_LAYER),
Name::new("PfShapeSdf"),
))
.id();
let second = (fill.is_some() && stroke.is_some()).then(|| {
commands
.spawn((
VectorPrimitive::Rect {
size: inner,
radius: inner_radius,
thickness: st.max(0.01),
color: stroke.unwrap().to_linear(),
},
HudTransform {
translation: centre + Vec3::new(0.0, 0.0, 1.0e-4),
..default()
},
RenderLayers::layer(SHAPE_LAYER),
Name::new("PfShapeSdfStroke"),
))
.id()
});
return (first, second);
}
}
let (path, style) = shape_to_vector(shape, px)
.unwrap_or_else(|| (Vec::new(), PathStyle::fill(LinearRgba::NONE)));
let draw = commands
.spawn((
VectorShape {
commands: path,
style,
},
transform(),
RenderLayers::layer(SHAPE_LAYER),
Name::new("PfShapeDraw"),
))
.id();
(draw, None)
}
#[allow(clippy::type_complexity)]
fn sync_gpu_shapes(
mut shapes: Query<(
Entity,
Ref<PfShape>,
&ComputedNode,
Option<&mut PfShapeGpu>,
Option<&PfShapeRendered>,
)>,
mut draws: Query<(&mut VectorShape, &mut HudTransform)>,
atlas: Option<ResMut<PfShapeAtlas>>,
mut layouts: ResMut<Assets<TextureAtlasLayout>>,
mut full: ResMut<PfAtlasFull>,
mut dirty: ResMut<PfAtlasDirty>,
mut commands: Commands,
) {
let Some(mut atlas) = atlas else { return };
for (entity, shape, computed, gpu, cpu_rendered) in &mut shapes {
let size = computed.size();
let px = UVec2::new(size.x.round() as u32, size.y.round() as u32);
if px.x == 0 || px.y == 0 {
continue;
}
let resized = gpu.as_ref().is_none_or(|g| g.size != px);
if !resized && !shape.is_changed() {
continue;
}
let Some((_path, _style)) = shape_to_vector(&shape, px) else {
continue;
};
let previous_draw: Vec<Entity> = gpu
.as_ref()
.map(|g| {
[Some(g.draw), g.draw_stroke]
.into_iter()
.flatten()
.collect()
})
.unwrap_or_default();
let old_slot = gpu.as_ref().map(|g| (g.capacity, g.origin));
if let Some(mut gpu) = gpu
&& px.cmple(gpu.capacity).all()
&& gpu.draw_stroke.is_none()
&& let Ok((mut vector, mut transform)) = draws.get_mut(gpu.draw)
{
let Some((path, style)) = shape_to_vector(&shape, px) else {
continue;
};
vector.commands = path;
vector.style = style;
dirty.0 = 2;
if gpu.size != px {
gpu.size = px;
transform.translation = slot_center_world(gpu.origin, px).extend(0.0);
if let Some(mut layout) = layouts.get_mut(&atlas.layout)
&& let Some(rect) = layout.textures.get_mut(gpu.index)
{
*rect = URect::from_corners(gpu.origin, gpu.origin + px);
}
}
continue;
}
if let Some((capacity, origin)) = old_slot {
atlas.release(capacity, origin);
}
let wanted = slot_capacity(px);
let Some((origin, capacity)) = atlas.allocate(wanted) else {
full.0 = true;
commands
.entity(entity)
.remove::<(PfShapeGpu, PfShapeClaim, ImageNode)>();
continue;
};
let Some(mut layout) = layouts.get_mut(&atlas.layout) else {
continue;
};
let index = layout.add_texture(URect::from_corners(origin, origin + px));
for previous in previous_draw {
commands.entity(previous).despawn();
}
let (draw, draw_stroke) = spawn_draws(&mut commands, &shape, px, origin);
dirty.0 = 2;
commands.entity(entity).insert((
ImageNode::from_atlas_image(
atlas.image.clone(),
TextureAtlas {
layout: atlas.layout.clone(),
index,
},
)
.with_mode(NodeImageMode::Stretch),
PfShapeGpu {
index,
origin,
capacity,
size: px,
draw,
draw_stroke,
},
PfShapeClaim {
backend: "vector_gpu",
},
));
if cpu_rendered.is_some() {
commands.entity(entity).remove::<PfShapeRendered>();
}
}
}
fn rebuild_atlas_if_full(
mut full: ResMut<PfAtlasFull>,
atlas: Option<ResMut<PfShapeAtlas>>,
mut layouts: ResMut<Assets<TextureAtlasLayout>>,
slots: Query<(Entity, &PfShapeGpu)>,
mut rebuilds: ResMut<PfAtlasRebuilds>,
mut commands: Commands,
) {
if !full.0 {
return;
}
full.0 = false;
rebuilds.0 = rebuilds.0.saturating_add(1);
let Some(mut atlas) = atlas else { return };
atlas.reset();
if let Some(mut layout) = layouts.get_mut(&atlas.layout) {
layout.textures.clear();
}
for (entity, gpu) in &slots {
commands.entity(gpu.draw).despawn();
if let Some(stroke) = gpu.draw_stroke {
commands.entity(stroke).despawn();
}
commands
.entity(entity)
.remove::<(PfShapeGpu, PfShapeClaim, ImageNode)>();
}
}
fn gate_atlas_camera(
mut dirty: ResMut<PfAtlasDirty>,
mut cameras: Query<&mut Camera, With<PfShapeAtlasCamera>>,
) {
dirty.0 = 0;
for mut camera in &mut cameras {
if !camera.is_active {
camera.is_active = true;
}
}
}