bevy_pf_vector 0.1.2

GPU vector rendering for bevy_pf: paths tessellated once at asset load and redrawn as instanced geometry, for the mostly-static topology of game HUD content.
//! bevy_pf_vector — a vector/UI rendering engine for bevy_pf.
//!
//! Original engine, one narrow bet: HUD content has mostly-static topology,
//! so paths are tessellated once (lyon) and redrawn as GPU-instanced geometry.
//! Read ARCHITECTURE.md before extending this.

pub mod painter;
pub mod path;
pub mod render;
pub mod tess;

use bevy::prelude::*;

pub use painter::{PainterConfig, VectorPainter, VectorPainterQueue};
pub use path::{
    Brush, DashPattern, FillRule, GradientStop, LineCap, LineJoin, PathCommand, PathStyle,
    StrokeStyle,
};

/// Marks a camera whose target is a TRANSPARENT texture that something else
/// will composite as STRAIGHT alpha — bevy_pf's shape atlas, sampled by
/// bevy_ui `ImageNode`s, is the case this exists for.
///
/// Without it, translucent vector content lands in such a texture
/// premultiplied: the blend pass is `SrcAlpha, OneMinusSrcAlpha` over a
/// cleared-to-transparent target, so the stored colour is already
/// `rgb * a`, and a consumer that multiplies by `a` again — every
/// straight-alpha compositor, bevy_ui included — shows the content at
/// roughly `a²`. An 8% fill reads as nothing; a 40% one as 16%.
///
/// On a marked camera the translucent pipelines blend `One,
/// OneMinusSrcAlpha` instead. A straight-colour fragment over the
/// transparent clear then stores its own colour and its own alpha, which
/// is exactly what a straight-alpha consumer expects. Where translucent
/// content overlaps translucent content in the SAME texture the result is
/// a close approximation rather than the exact `over` (fixed-function
/// blending cannot divide by the destination alpha); opaque-over-anything
/// and anything-over-nothing are exact, which is what HUD chrome is made
/// of. Do NOT mark a camera that draws to the screen or over an opaque
/// scene: there the ordinary blend is the correct one.
#[derive(
    Component, Clone, Copy, Debug, Default, bevy::render::extract_component::ExtractComponent,
)]
pub struct StraightAlphaTarget;

/// Flat 2D transform for HUD elements. Use INSTEAD of animating `Transform`:
/// entities animated through this component never dirty the transform
/// hierarchy, so Bevy's propagation systems have nothing to do — the last
/// engine-controllable frame cost for large animated HUDs. Entities without
/// a parent/children relationship should prefer it; `Transform`-driven
/// entities (hierarchies like a compass with tick children) keep working
/// unchanged, and when both are present this one wins.
#[derive(Component, Clone, Copy, Debug)]
pub struct HudTransform {
    pub translation: Vec3,
    /// Rotation around Z in radians.
    pub rotation: f32,
    pub scale: Vec2,
}

impl Default for HudTransform {
    fn default() -> Self {
        Self { translation: Vec3::ZERO, rotation: 0.0, scale: Vec2::ONE }
    }
}

impl HudTransform {
    pub fn from_xyz(x: f32, y: f32, z: f32) -> Self {
        Self { translation: Vec3::new(x, y, z), ..Default::default() }
    }

    /// (2x2 column-major linear part, translation.xy, z) for extraction.
    pub(crate) fn decompose(&self) -> ([f32; 4], [f32; 2], f32) {
        let (sin, cos) = bevy::math::ops::sin_cos(self.rotation);
        (
            [
                cos * self.scale.x,
                sin * self.scale.x,
                -sin * self.scale.y,
                cos * self.scale.y,
            ],
            [self.translation.x, self.translation.y],
            self.translation.z,
        )
    }
}

/// A vector shape authored as a path outline plus style. Topology is treated
/// as static: the path is tessellated once when the component is added; if
/// the component is mutated, the shape re-tessellates that frame (priced per
/// changed shape). For continuously parameter-animated primitives, prefer
/// [`VectorPrimitive`] — those never tessellate at all.
#[derive(Component, Clone, Debug)]
#[require(Transform)]
pub struct VectorShape {
    pub commands: Vec<PathCommand>,
    pub style: PathStyle,
}

/// Parametric primitives evaluated entirely in the vertex shader from a
/// canonical mesh — animating their parameters costs one instance write per
/// frame, no tessellation, and all instances of a primitive kind draw in a
/// single instanced call. The gauge/meter fast path.
#[derive(Component, Clone, Copy, Debug)]
#[require(Transform)]
pub enum VectorPrimitive {
    /// Ring segment. Angles in radians, y-up, counter-clockwise.
    Arc {
        inner: f32,
        outer: f32,
        start: f32,
        sweep: f32,
        color: LinearRgba,
    },
    /// Rounded rectangle evaluated as a signed distance field: ONE quad,
    /// no tessellation, and `size` is per-instance — so a bar or meter that
    /// resizes every frame costs one instance write instead of a fresh
    /// tessellation. `thickness > 0` strokes inward from the edge;
    /// `thickness <= 0` fills.
    ///
    /// Degenerates into the other primitives, which is why there is only one:
    /// `radius == 0` is a rect, `radius == min(size)/2` is a circle or
    /// capsule, and a thin rotated one is a line with round caps. Prefer the
    /// [`VectorPrimitive::circle`] / [`VectorPrimitive::line`] constructors
    /// for readability.
    Rect {
        size: Vec2,
        radius: f32,
        thickness: f32,
        color: LinearRgba,
    },
}

impl VectorPrimitive {
    /// Filled or stroked circle.
    pub fn circle(radius: f32, thickness: f32, color: LinearRgba) -> Self {
        Self::Rect {
            size: Vec2::splat(radius * 2.0),
            radius,
            thickness,
            color,
        }
    }

    /// Line with round caps, as a capsule. The caller positions and rotates
    /// it with the entity transform; `length` is along local X.
    pub fn line(length: f32, thickness: f32, color: LinearRgba) -> Self {
        Self::Rect {
            size: Vec2::new(length + thickness, thickness),
            radius: thickness * 0.5,
            // A capsule is the FILLED shape; stroking it would outline the
            // line rather than draw it.
            thickness: 0.0,
            color,
        }
    }
}

/// A clip region. Entities with this component don't render; content
/// references them via [`ClippedBy`]. Clips nest by putting `ClippedBy` on a
/// clip entity itself (up to 4 levels). Evaluated analytically in the
/// fragment shader — clip edges are antialiased and clipping costs no extra
/// draw calls, state changes, or stencil passes.
#[derive(Component, Clone, Copy, Debug)]
#[require(Transform)]
pub enum VectorClipShape {
    RoundedRect { half_extents: Vec2, radius: f32 },
    Circle { radius: f32 },
}

/// Clips the entity's rendering to the referenced [`VectorClipShape`] entity
/// (and that clip's own ancestors, if it is itself clipped).
#[derive(Component, Clone, Copy, Debug)]
pub struct ClippedBy(pub Entity);

pub struct PfVectorPlugin;

impl Plugin for PfVectorPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<painter::VectorPainterQueue>()
            // Cleared at the top of the frame so `Update`-schedule painting
            // is what the following extract sees.
            .add_systems(First, painter::clear_painter_queue)
            .add_plugins(render::VectorRenderPlugin);
    }
}