// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
pub struct Color {
  r : Float
  g : Float
  b : Float
  a : Float
}

///|
pub fn Color::new(r : Float, g : Float, b : Float, a : Float) -> Color {
  Color::{ r, g, b, a }
}

///|
pub fn Color::white() -> Color {
  Color::new(1.0F, 1.0F, 1.0F, 1.0F)
}

///|
pub fn Color::black() -> Color {
  Color::new(0.0F, 0.0F, 0.0F, 1.0F)
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct RenderLayers {
  mask : Int
}

///|
pub fn RenderLayers::default() -> RenderLayers {
  RenderLayers::{ mask: 1 }
}

///|
pub fn RenderLayers::layer(index : Int) -> RenderLayers {
  let clamped = if index < 0 { 0 } else { index }
  RenderLayers::{ mask: 1 << clamped }
}

///|
pub fn RenderLayers::matches(self : RenderLayers, other : RenderLayers) -> Bool {
  (self.mask & other.mask) != 0
}

///|
pub struct Viewport {
  physical_position : @math.IVec2
  physical_size : @math.IVec2
}

///|
pub fn Viewport::new(
  physical_position : @math.IVec2,
  physical_size : @math.IVec2,
) -> Viewport {
  Viewport::{ physical_position, physical_size }
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Sprite {
  image : @asset.Handle[@asset.Image]
  texture_atlas : @asset.TextureAtlas?
  color : Color
  flip_x : Bool
  flip_y : Bool
  custom_size : @math.Vec2?
  rect : @math.Rect?
  image_mode : SpriteImageMode
}

///|
pub struct BorderRect {
  left : Float
  right : Float
  top : Float
  bottom : Float
}

///|
pub fn BorderRect::new(
  left : Float,
  right : Float,
  top : Float,
  bottom : Float,
) -> BorderRect {
  BorderRect::{ left, right, top, bottom }
}

///|
pub fn BorderRect::square(value : Float) -> BorderRect {
  BorderRect::{ left: value, right: value, top: value, bottom: value }
}

///|
pub fn BorderRect::default() -> BorderRect {
  BorderRect::square(0.0F)
}

///|
pub(all) enum SliceScaleMode {
  Stretch
  Tile(Float)
}

///|
pub fn SliceScaleMode::default() -> SliceScaleMode {
  SliceScaleMode::Stretch
}

///|
pub struct SpriteTiling {
  tile_x : Bool
  tile_y : Bool
  stretch_value : Float
}

///|
pub fn SpriteTiling::new(
  tile_x : Bool,
  tile_y : Bool,
  stretch_value : Float,
) -> SpriteTiling {
  SpriteTiling::{ tile_x, tile_y, stretch_value }
}

///|
pub fn SpriteTiling::default() -> SpriteTiling {
  SpriteTiling::new(true, true, 1.0F)
}

///|
pub fn SpriteTiling::with_tile_x(
  self : SpriteTiling,
  tile_x : Bool,
) -> SpriteTiling {
  SpriteTiling::{ ..self, tile_x, }
}

///|
pub fn SpriteTiling::with_tile_y(
  self : SpriteTiling,
  tile_y : Bool,
) -> SpriteTiling {
  SpriteTiling::{ ..self, tile_y, }
}

///|
pub fn SpriteTiling::with_stretch_value(
  self : SpriteTiling,
  stretch_value : Float,
) -> SpriteTiling {
  SpriteTiling::{ ..self, stretch_value, }
}

///|
pub struct TextureSlicer {
  border : BorderRect
  center_scale_mode : SliceScaleMode
  sides_scale_mode : SliceScaleMode
  max_corner_scale : Float
}

///|
pub fn TextureSlicer::new(border : BorderRect) -> TextureSlicer {
  TextureSlicer::{
    border,
    center_scale_mode: SliceScaleMode::Stretch,
    sides_scale_mode: SliceScaleMode::Stretch,
    max_corner_scale: 1.0F,
  }
}

///|
pub fn TextureSlicer::default() -> TextureSlicer {
  TextureSlicer::new(BorderRect::default())
}

///|
pub fn TextureSlicer::with_center_scale_mode(
  self : TextureSlicer,
  center_scale_mode : SliceScaleMode,
) -> TextureSlicer {
  TextureSlicer::{ ..self, center_scale_mode, }
}

///|
pub fn TextureSlicer::with_sides_scale_mode(
  self : TextureSlicer,
  sides_scale_mode : SliceScaleMode,
) -> TextureSlicer {
  TextureSlicer::{ ..self, sides_scale_mode, }
}

///|
pub fn TextureSlicer::with_max_corner_scale(
  self : TextureSlicer,
  max_corner_scale : Float,
) -> TextureSlicer {
  TextureSlicer::{ ..self, max_corner_scale, }
}

///|
pub(all) enum SpriteImageMode {
  Auto
  Sliced(TextureSlicer)
  Tiled(SpriteTiling)
}

///|
pub fn SpriteImageMode::default() -> SpriteImageMode {
  SpriteImageMode::Auto
}

///|
fn sprite_image_mode_uses_slices(mode : SpriteImageMode) -> Bool {
  match mode {
    SpriteImageMode::Sliced(_) => true
    _ => false
  }
}

///|
pub fn SpriteImageMode::tiled(
  tile_x? : Bool = true,
  tile_y? : Bool = true,
  stretch_value? : Float = 1.0F,
) -> SpriteImageMode {
  SpriteImageMode::Tiled(SpriteTiling::new(tile_x, tile_y, stretch_value))
}

///|
pub fn Sprite::from_image(image : @asset.Handle[@asset.Image]) -> Sprite {
  Sprite::{
    image,
    texture_atlas: None,
    color: Color::white(),
    flip_x: false,
    flip_y: false,
    custom_size: None,
    rect: None,
    image_mode: SpriteImageMode::Auto,
  }
}

///|
pub fn Sprite::from_atlas_image(
  image : @asset.Handle[@asset.Image],
  atlas : @asset.TextureAtlas,
) -> Sprite {
  Sprite::{ ..Sprite::from_image(image), texture_atlas: Some(atlas) }
}

///|
pub fn Sprite::with_texture_atlas(
  self : Sprite,
  texture_atlas : @asset.TextureAtlas?,
) -> Sprite {
  Sprite::{ ..self, texture_atlas, }
}

///|
pub fn Sprite::with_color(self : Sprite, color : Color) -> Sprite {
  Sprite::{ ..self, color, }
}

///|
pub fn Sprite::with_flip_x(self : Sprite, flip_x : Bool) -> Sprite {
  Sprite::{ ..self, flip_x, }
}

///|
pub fn Sprite::with_flip_y(self : Sprite, flip_y : Bool) -> Sprite {
  Sprite::{ ..self, flip_y, }
}

///|
pub fn Sprite::with_custom_size(
  self : Sprite,
  custom_size : @math.Vec2?,
) -> Sprite {
  Sprite::{ ..self, custom_size, }
}

///|
pub fn Sprite::with_rect(self : Sprite, rect : @math.Rect?) -> Sprite {
  Sprite::{ ..self, rect, }
}

///|
pub fn Sprite::with_image_mode(
  self : Sprite,
  image_mode : SpriteImageMode,
) -> Sprite {
  Sprite::{ ..self, image_mode, }
}

///|
/// A per-entity scissor rectangle in *logical* pixels, relative to the active
/// camera's view (top-left origin).
///
/// When present, `render_execute_system` converts it into physical pixels using
/// the camera's scale factor and viewport offset, then applies it via
/// `host_gpu_set_scissor`.
///
/// This is primarily used by UI clipping (overflow/scroll).
#ecs.component
pub struct ScissorRect {
  rect : @math.Rect
}

///|
pub fn ScissorRect::new(rect : @math.Rect) -> ScissorRect {
  ScissorRect::{ rect, }
}

///|
/// UI corner radii (top-left, top-right, bottom-right, bottom-left) in logical pixels.
pub struct UiCornerRadii {
  top_left : Float
  top_right : Float
  bottom_right : Float
  bottom_left : Float
}

///|
pub fn UiCornerRadii::new(
  top_left : Float,
  top_right : Float,
  bottom_right : Float,
  bottom_left : Float,
) -> UiCornerRadii {
  UiCornerRadii::{ top_left, top_right, bottom_right, bottom_left }
}

///|
pub fn UiCornerRadii::zero() -> UiCornerRadii {
  UiCornerRadii::new(0.0F, 0.0F, 0.0F, 0.0F)
}

///|
/// UI border thickness (left, top, right, bottom) in logical pixels.
pub struct UiBorderRect {
  left : Float
  top : Float
  right : Float
  bottom : Float
}

///|
pub fn UiBorderRect::new(
  left : Float,
  top : Float,
  right : Float,
  bottom : Float,
) -> UiBorderRect {
  UiBorderRect::{ left, top, right, bottom }
}

///|
pub fn UiBorderRect::zero() -> UiBorderRect {
  UiBorderRect::new(0.0F, 0.0F, 0.0F, 0.0F)
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Anchor {
  value : @math.Vec2
}

///|
pub fn Anchor::new(value : @math.Vec2) -> Anchor {
  Anchor::{ value, }
}

///|
pub fn Anchor::as_vec(self : Anchor) -> @math.Vec2 {
  self.value
}

///|
pub fn Anchor::bottom_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, -0.5F))
}

///|
pub fn Anchor::bottom_center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, -0.5F))
}

///|
pub fn Anchor::bottom_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, -0.5F))
}

///|
pub fn Anchor::center_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, 0.0F))
}

///|
pub fn Anchor::center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, 0.0F))
}

///|
pub fn Anchor::center_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, 0.0F))
}

///|
pub fn Anchor::top_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, 0.5F))
}

///|
pub fn Anchor::top_center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, 0.5F))
}

///|
pub fn Anchor::top_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, 0.5F))
}

///|
pub fn Anchor::default() -> Anchor {
  Anchor::center()
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Camera {
  viewport : Viewport?
  order : Int
  clear_color : Color
}

///|
pub fn Camera::new(clear_color : Color) -> Camera {
  Camera::{ viewport: None, order: 0, clear_color }
}

///|
pub fn Camera::default() -> Camera {
  Camera::new(Color::new(0.5F, 0.5F, 0.5F, 1.0F))
}

///|
pub fn Camera::with_order(self : Camera, order : Int) -> Camera {
  Camera::{ ..self, order, }
}

///|
pub fn Camera::with_viewport(self : Camera, viewport : Viewport) -> Camera {
  Camera::{ ..self, viewport: Some(viewport) }
}

///|
pub fn Camera::clear_viewport(self : Camera) -> Camera {
  Camera::{ ..self, viewport: None }
}

///|
/// The projection parameters for an orthographic camera.
/// Stored inside `Projection` (mirrors Bevy's `Projection::Orthographic`).
pub struct OrthographicProjection {
  scale : Float
}

///|
pub fn OrthographicProjection::new(scale : Float) -> OrthographicProjection {
  let safe = if scale == 0.0F { 1.0F } else { scale }
  OrthographicProjection::{ scale: safe }
}

///|
pub fn OrthographicProjection::default_2d() -> OrthographicProjection {
  OrthographicProjection::new(1.0F)
}

///|
pub fn OrthographicProjection::with_scale(
  self : OrthographicProjection,
  scale : Float,
) -> OrthographicProjection {
  self |> ignore
  OrthographicProjection::new(scale)
}

///|
/// Camera projection type (mirrors Bevy's `Projection` component).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum Projection {
  Orthographic(OrthographicProjection)
}

///|
pub fn Projection::orthographic_2d() -> Projection {
  Orthographic(OrthographicProjection::default_2d())
}

///|
/// Marker component that enables 2D camera semantics (mirrors Bevy's `Camera2d`).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Camera2d {}

///|
pub fn Camera2d::default() -> Camera2d {
  Camera2d::{  }
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum RenderTarget {
  Screen
  Image(@asset.Handle[@asset.Image])
}

///|
pub struct ColorMaterialAsset {
  color : Color
  alpha_mode : AlphaMode2d
  texture : @asset.Handle[@asset.Image]?
  uv_offset : @math.Vec2
  uv_scale : @math.Vec2
}

///|
pub(all) enum AlphaMode2d {
  Opaque
  Mask(Float)
  Blend
}

///|
pub fn ColorMaterialAsset::new(
  color : Color,
  alpha_mode : AlphaMode2d,
  texture? : @asset.Handle[@asset.Image]? = None,
  uv_offset? : @math.Vec2 = @math.Vec2::new(0.0F, 0.0F),
  uv_scale? : @math.Vec2 = @math.Vec2::new(1.0F, 1.0F),
) -> ColorMaterialAsset {
  ColorMaterialAsset::{ color, alpha_mode, texture, uv_offset, uv_scale }
}

///|
let color_material_assets : Ref[@asset.Assets[ColorMaterialAsset]] = Ref::new(
  @asset.Assets::new(),
)

///|
fn color_material_get(
  handle : @asset.Handle[ColorMaterialAsset],
) -> ColorMaterialAsset? {
  color_material_assets.val.get(handle)
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ColorMaterial {
  handle : @asset.Handle[ColorMaterialAsset]
}

///|
fn color_material_add(
  color : Color,
  alpha_mode : AlphaMode2d,
  texture : @asset.Handle[@asset.Image]?,
  uv_offset : @math.Vec2,
  uv_scale : @math.Vec2,
) -> ColorMaterial {
  let handle = color_material_assets.val.add(
    ColorMaterialAsset::new(color, alpha_mode, texture~, uv_offset~, uv_scale~),
  )
  ColorMaterial::{ handle, }
}

///|
pub fn ColorMaterial::new(color : Color) -> ColorMaterial {
  color_material_add(
    color,
    AlphaMode2d::Blend,
    None,
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(1.0F, 1.0F),
  )
}

///|
pub fn ColorMaterial::from_image(
  texture : @asset.Handle[@asset.Image],
) -> ColorMaterial {
  color_material_add(
    Color::white(),
    AlphaMode2d::Blend,
    Some(texture),
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(1.0F, 1.0F),
  )
}

///|
pub fn ColorMaterial::color(self : ColorMaterial) -> Color {
  match color_material_get(self.handle) {
    Some(mat) => mat.color
    None => Color::white()
  }
}

///|
pub fn ColorMaterial::new_with_alpha_mode(
  color : Color,
  alpha_mode : AlphaMode2d,
) -> ColorMaterial {
  color_material_add(
    color,
    alpha_mode,
    None,
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(1.0F, 1.0F),
  )
}

///|
pub fn ColorMaterial::with_alpha_mode(
  self : ColorMaterial,
  alpha_mode : AlphaMode2d,
) -> ColorMaterial {
  color_material_add(
    self.color(),
    alpha_mode,
    self.texture(),
    self.uv_offset(),
    self.uv_scale(),
  )
}

///|
pub fn ColorMaterial::alpha_mode(self : ColorMaterial) -> AlphaMode2d {
  match color_material_get(self.handle) {
    Some(mat) => mat.alpha_mode
    None => AlphaMode2d::Blend
  }
}

///|
pub fn ColorMaterial::texture(
  self : ColorMaterial,
) -> @asset.Handle[@asset.Image]? {
  match color_material_get(self.handle) {
    Some(mat) => mat.texture
    None => None
  }
}

///|
pub fn ColorMaterial::uv_offset(self : ColorMaterial) -> @math.Vec2 {
  match color_material_get(self.handle) {
    Some(mat) => mat.uv_offset
    None => @math.Vec2::new(0.0F, 0.0F)
  }
}

///|
pub fn ColorMaterial::uv_scale(self : ColorMaterial) -> @math.Vec2 {
  match color_material_get(self.handle) {
    Some(mat) => mat.uv_scale
    None => @math.Vec2::new(1.0F, 1.0F)
  }
}

///|
pub fn ColorMaterial::with_texture(
  self : ColorMaterial,
  texture : @asset.Handle[@asset.Image],
) -> ColorMaterial {
  color_material_add(
    self.color(),
    self.alpha_mode(),
    Some(texture),
    self.uv_offset(),
    self.uv_scale(),
  )
}

///|
pub fn ColorMaterial::with_uv_offset(
  self : ColorMaterial,
  uv_offset : @math.Vec2,
) -> ColorMaterial {
  color_material_add(
    self.color(),
    self.alpha_mode(),
    self.texture(),
    uv_offset,
    self.uv_scale(),
  )
}

///|
pub fn ColorMaterial::with_uv_scale(
  self : ColorMaterial,
  uv_scale : @math.Vec2,
) -> ColorMaterial {
  color_material_add(
    self.color(),
    self.alpha_mode(),
    self.texture(),
    self.uv_offset(),
    uv_scale,
  )
}

///|
pub struct Capsule2d {
  radius : Float
  half_length : Float
  segments : Int
}

///|
pub fn Capsule2d::new(
  radius : Float,
  half_length : Float,
  segments : Int,
) -> Capsule2d {
  Capsule2d::{ radius, half_length, segments }
}

///|
pub fn Capsule2d::default() -> Capsule2d {
  Capsule2d::new(0.5F, 0.5F, 16)
}

///|
pub struct Rectangle {
  half_size : @math.Vec2
}

///|
pub fn Rectangle::new(width : Float, height : Float) -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(width / 2.0F, height / 2.0F) }
}

///|
pub fn Rectangle::from_size(size : @math.Vec2) -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(size.x / 2.0F, size.y / 2.0F) }
}

///|
pub fn Rectangle::default() -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(0.5F, 0.5F) }
}

///|
pub struct Circle {
  radius : Float
  vertices : Int
}

///|
pub fn Circle::new(radius : Float) -> Circle {
  Circle::{ radius, vertices: 32 }
}

///|
pub fn Circle::with_vertices(self : Circle, vertices : Int) -> Circle {
  Circle::{ ..self, vertices, }
}

///|
pub fn Circle::default() -> Circle {
  Circle::new(0.5F)
}

///|
pub struct Ellipse {
  half_size : @math.Vec2
  vertices : Int
}

///|
pub fn Ellipse::new(half_width : Float, half_height : Float) -> Ellipse {
  Ellipse::{ half_size: @math.Vec2::new(half_width, half_height), vertices: 32 }
}

///|
pub fn Ellipse::with_vertices(self : Ellipse, vertices : Int) -> Ellipse {
  Ellipse::{ ..self, vertices, }
}

///|
pub fn Ellipse::default() -> Ellipse {
  Ellipse::new(0.5F, 0.25F)
}

///|
pub struct Annulus {
  inner_radius : Float
  outer_radius : Float
  vertices : Int
}

///|
pub fn Annulus::new(inner_radius : Float, outer_radius : Float) -> Annulus {
  Annulus::{ inner_radius, outer_radius, vertices: 48 }
}

///|
pub fn Annulus::with_vertices(self : Annulus, vertices : Int) -> Annulus {
  Annulus::{ ..self, vertices, }
}

///|
pub fn Annulus::default() -> Annulus {
  Annulus::new(0.25F, 0.5F)
}

///|
pub struct Ring2d {
  outer_radius : Float
  inner_radius : Float
  vertices : Int
}

///|
pub fn Ring2d::new(outer_radius : Float, inner_radius : Float) -> Ring2d {
  Ring2d::{ outer_radius, inner_radius, vertices: 48 }
}

///|
pub fn Ring2d::with_vertices(self : Ring2d, vertices : Int) -> Ring2d {
  Ring2d::{ ..self, vertices, }
}

///|
pub fn Ring2d::default() -> Ring2d {
  Ring2d::new(0.5F, 0.25F)
}

///|
pub struct Rhombus {
  half_diagonal : @math.Vec2
}

///|
pub fn Rhombus::new(width : Float, height : Float) -> Rhombus {
  Rhombus::{ half_diagonal: @math.Vec2::new(width / 2.0F, height / 2.0F) }
}

///|
pub fn Rhombus::default() -> Rhombus {
  Rhombus::new(1.0F, 1.0F)
}

///|
pub struct RegularPolygon {
  radius : Float
  sides : Int
}

///|
pub fn RegularPolygon::new(radius : Float, sides : Int) -> RegularPolygon {
  RegularPolygon::{ radius, sides }
}

///|
pub fn RegularPolygon::default() -> RegularPolygon {
  RegularPolygon::new(0.5F, 6)
}

///|
pub struct Triangle2d {
  a : @math.Vec2
  b : @math.Vec2
  c : @math.Vec2
}

///|
pub fn Triangle2d::new(
  a : @math.Vec2,
  b : @math.Vec2,
  c : @math.Vec2,
) -> Triangle2d {
  Triangle2d::{ a, b, c }
}

///|
pub fn Triangle2d::default() -> Triangle2d {
  Triangle2d::{
    a: @math.Vec2::new(0.0F, 0.5F),
    b: @math.Vec2::new(-0.5F, -0.5F),
    c: @math.Vec2::new(0.5F, -0.5F),
  }
}

///|
pub struct Segment2d {
  start : @math.Vec2
  end : @math.Vec2
  half_thickness : Float
}

///|
pub fn Segment2d::new(start : @math.Vec2, end : @math.Vec2) -> Segment2d {
  Segment2d::{ start, end, half_thickness: 2.5F }
}

///|
pub fn Segment2d::with_half_thickness(
  self : Segment2d,
  half_thickness : Float,
) -> Segment2d {
  Segment2d::{ ..self, half_thickness, }
}

///|
pub struct Polyline2d {
  points : Array[@math.Vec2]
  half_thickness : Float
}

///|
pub fn Polyline2d::new(points : Array[@math.Vec2]) -> Polyline2d {
  Polyline2d::{ points, half_thickness: 2.5F }
}

///|
pub fn Polyline2d::with_half_thickness(
  self : Polyline2d,
  half_thickness : Float,
) -> Polyline2d {
  Polyline2d::{ ..self, half_thickness, }
}

///|
pub struct CircularSector {
  radius : Float
  half_angle : Float
  vertices : Int
}

///|
pub fn CircularSector::new(
  radius : Float,
  half_angle : Float,
) -> CircularSector {
  CircularSector::{ radius, half_angle, vertices: 32 }
}

///|
pub fn CircularSector::from_turns(
  radius : Float,
  fraction : Float,
) -> CircularSector {
  CircularSector::new(radius, fraction * @math.pi_half() * 2.0F)
}

///|
pub fn CircularSector::with_vertices(
  self : CircularSector,
  vertices : Int,
) -> CircularSector {
  CircularSector::{ ..self, vertices, }
}

///|
pub fn CircularSector::half_angle(self : CircularSector) -> Float {
  self.half_angle
}

///|
pub fn CircularSector::angle(self : CircularSector) -> Float {
  self.half_angle * 2.0F
}

///|
pub fn CircularSector::default() -> CircularSector {
  CircularSector::new(0.5F, @math.pi_half() * 2.0F / 3.0F)
}

///|
pub struct CircularSegment {
  radius : Float
  half_angle : Float
  vertices : Int
}

///|
pub fn CircularSegment::new(
  radius : Float,
  half_angle : Float,
) -> CircularSegment {
  CircularSegment::{ radius, half_angle, vertices: 32 }
}

///|
pub fn CircularSegment::from_turns(
  radius : Float,
  fraction : Float,
) -> CircularSegment {
  CircularSegment::new(radius, fraction * @math.pi_half() * 2.0F)
}

///|
pub fn CircularSegment::with_vertices(
  self : CircularSegment,
  vertices : Int,
) -> CircularSegment {
  CircularSegment::{ ..self, vertices, }
}

///|
pub fn CircularSegment::half_angle(self : CircularSegment) -> Float {
  self.half_angle
}

///|
pub fn CircularSegment::angle(self : CircularSegment) -> Float {
  self.half_angle * 2.0F
}

///|
pub fn CircularSegment::default() -> CircularSegment {
  CircularSegment::new(0.5F, @math.pi_half() * 2.0F / 3.0F)
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Mesh2d {
  mesh : @asset.Handle[@mesh.Mesh]
}

///|
let mesh_assets : Ref[@asset.Assets[@mesh.Mesh]] = Ref::new(
  @asset.Assets::new(),
)

///|
fn mesh_asset_add(mesh : @mesh.Mesh) -> @asset.Handle[@mesh.Mesh] {
  mesh_assets.val.add(mesh)
}

///|
fn mesh_asset_get(handle : @asset.Handle[@mesh.Mesh]) -> @mesh.Mesh? {
  mesh_assets.val.get(handle)
}

///|
pub fn Mesh2d::new(mesh : @asset.Handle[@mesh.Mesh]) -> Mesh2d {
  Mesh2d::{ mesh, }
}

///|
fn rectangle_to_mesh2d_geometry(rectangle : Rectangle) -> @mesh.Mesh2dGeometry {
  let hx = rectangle.half_size.x
  let hy = rectangle.half_size.y
  let positions = [
    @math.Vec2::new(-hx, -hy),
    @math.Vec2::new(hx, -hy),
    @math.Vec2::new(hx, hy),
    @math.Vec2::new(-hx, hy),
  ]
  let uvs = [
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(1.0F, 0.0F),
    @math.Vec2::new(1.0F, 1.0F),
    @math.Vec2::new(0.0F, 1.0F),
  ]
  let indices = [0, 1, 2, 0, 2, 3]
  @mesh.Mesh2dGeometry::new(positions, uvs, Some(indices))
}

///|
fn shape_abs_float(v : Float) -> Float {
  if v < 0.0F {
    -v
  } else {
    v
  }
}

///|
fn shape_safe_positive(v : Float, fallback : Float) -> Float {
  if v <= 0.0F {
    fallback
  } else {
    v
  }
}

///|
fn shape_safe_segments(v : Int, minimum : Int) -> Int {
  if v < minimum {
    minimum
  } else {
    v
  }
}

///|
fn shape_tau() -> Float {
  @math.pi_half() * 4.0F
}

///|
fn shape_positions_to_uvs(positions : Array[@math.Vec2]) -> Array[@math.Vec2] {
  if positions.length() == 0 {
    return []
  }
  let mut min_x = positions[0].x
  let mut max_x = positions[0].x
  let mut min_y = positions[0].y
  let mut max_y = positions[0].y
  for p in positions {
    if p.x < min_x {
      min_x = p.x
    }
    if p.x > max_x {
      max_x = p.x
    }
    if p.y < min_y {
      min_y = p.y
    }
    if p.y > max_y {
      max_y = p.y
    }
  }
  let dx = if max_x - min_x == 0.0F { 1.0F } else { max_x - min_x }
  let dy = if max_y - min_y == 0.0F { 1.0F } else { max_y - min_y }
  let uvs : Array[@math.Vec2] = []
  for p in positions {
    let u = (p.x - min_x) / dx
    let v = (p.y - min_y) / dy
    uvs.push(@math.Vec2::new(u, v))
  }
  uvs
}

///|
fn shape_geometry_from_nonindexed_positions(
  positions : Array[@math.Vec2],
) -> @mesh.Mesh2dGeometry {
  let safe_positions : Array[@math.Vec2] = []
  if positions.length() >= 3 {
    for p in positions {
      safe_positions.push(p)
    }
  } else {
    safe_positions.push(@math.Vec2::new(-0.5F, -0.5F))
    safe_positions.push(@math.Vec2::new(0.5F, -0.5F))
    safe_positions.push(@math.Vec2::new(0.0F, 0.5F))
  }
  let uvs = shape_positions_to_uvs(safe_positions)
  @mesh.Mesh2dGeometry::new(safe_positions, uvs, None)
}

///|
fn shape_triangulate_fan(perimeter : Array[@math.Vec2]) -> Array[@math.Vec2] {
  let out : Array[@math.Vec2] = []
  let n = perimeter.length()
  if n < 3 {
    return out
  }
  let pivot = perimeter[0]
  for i in 1..<(n - 1) {
    out.push(pivot)
    out.push(perimeter[i])
    out.push(perimeter[i + 1])
  }
  out
}

///|
fn shape_perimeter_circle(radius : Float, segments : Int) -> Array[@math.Vec2] {
  let perimeter : Array[@math.Vec2] = []
  let tau = shape_tau()
  for i in 0.. Array[@math.Vec2] {
  let perimeter : Array[@math.Vec2] = []
  let tau = shape_tau()
  for i in 0.. @mesh.Mesh2dGeometry {
  let radius = shape_safe_positive(shape_abs_float(circle.radius), 0.5F)
  let segments = shape_safe_segments(circle.vertices, 3)
  shape_geometry_from_nonindexed_positions(
    shape_triangulate_fan(shape_perimeter_circle(radius, segments)),
  )
}

///|
fn ellipse_to_mesh2d_geometry(ellipse : Ellipse) -> @mesh.Mesh2dGeometry {
  let hx = shape_safe_positive(shape_abs_float(ellipse.half_size.x), 0.5F)
  let hy = shape_safe_positive(shape_abs_float(ellipse.half_size.y), 0.5F)
  let segments = shape_safe_segments(ellipse.vertices, 3)
  shape_geometry_from_nonindexed_positions(
    shape_triangulate_fan(
      shape_perimeter_ellipse(@math.Vec2::new(hx, hy), segments),
    ),
  )
}

///|
fn annulus_to_mesh2d_geometry(annulus : Annulus) -> @mesh.Mesh2dGeometry {
  let mut inner = shape_abs_float(annulus.inner_radius)
  let mut outer = shape_abs_float(annulus.outer_radius)
  if outer < inner {
    let pair = swap_float_pair(inner, outer)
    inner = pair.0
    outer = pair.1
  }
  outer = shape_safe_positive(outer, 0.5F)
  if inner >= outer {
    inner = outer * 0.5F
  }
  let segments = shape_safe_segments(annulus.vertices, 3)
  let tau = shape_tau()
  let triangles : Array[@math.Vec2] = []
  for i in 0.. @mesh.Mesh2dGeometry {
  annulus_to_mesh2d_geometry(
    Annulus::new(ring.inner_radius, ring.outer_radius).with_vertices(
      ring.vertices,
    ),
  )
}

///|
fn rhombus_to_mesh2d_geometry(rhombus : Rhombus) -> @mesh.Mesh2dGeometry {
  let hx = shape_safe_positive(shape_abs_float(rhombus.half_diagonal.x), 0.5F)
  let hy = shape_safe_positive(shape_abs_float(rhombus.half_diagonal.y), 0.5F)
  let perimeter = [
    @math.Vec2::new(0.0F, hy),
    @math.Vec2::new(hx, 0.0F),
    @math.Vec2::new(0.0F, -hy),
    @math.Vec2::new(-hx, 0.0F),
  ]
  shape_geometry_from_nonindexed_positions(shape_triangulate_fan(perimeter))
}

///|
fn regular_polygon_to_mesh2d_geometry(
  polygon : RegularPolygon,
) -> @mesh.Mesh2dGeometry {
  let radius = shape_safe_positive(shape_abs_float(polygon.radius), 0.5F)
  let sides = shape_safe_segments(polygon.sides, 3)
  let perimeter : Array[@math.Vec2] = []
  let tau = shape_tau()
  for i in 0.. @mesh.Mesh2dGeometry {
  shape_geometry_from_nonindexed_positions([triangle.a, triangle.b, triangle.c])
}

///|
fn shape_segment_triangles(
  start : @math.Vec2,
  end : @math.Vec2,
  half_thickness : Float,
) -> Array[@math.Vec2] {
  let triangles : Array[@math.Vec2] = []
  let thickness = shape_safe_positive(shape_abs_float(half_thickness), 0.5F)
  let dx = end.x - start.x
  let dy = end.y - start.y
  let len = @math.sqrt(dx * dx + dy * dy)
  if len <= 0.0001F {
    let p0 = @math.Vec2::new(start.x - thickness, start.y - thickness)
    let p1 = @math.Vec2::new(start.x + thickness, start.y - thickness)
    let p2 = @math.Vec2::new(start.x + thickness, start.y + thickness)
    let p3 = @math.Vec2::new(start.x - thickness, start.y + thickness)
    triangles.push(p0)
    triangles.push(p1)
    triangles.push(p2)
    triangles.push(p0)
    triangles.push(p2)
    triangles.push(p3)
    return triangles
  }
  let nx = -dy / len * thickness
  let ny = dx / len * thickness
  let p0 = @math.Vec2::new(start.x + nx, start.y + ny)
  let p1 = @math.Vec2::new(end.x + nx, end.y + ny)
  let p2 = @math.Vec2::new(end.x - nx, end.y - ny)
  let p3 = @math.Vec2::new(start.x - nx, start.y - ny)
  triangles.push(p0)
  triangles.push(p1)
  triangles.push(p2)
  triangles.push(p0)
  triangles.push(p2)
  triangles.push(p3)
  triangles
}

///|
fn segment_to_mesh2d_geometry(segment : Segment2d) -> @mesh.Mesh2dGeometry {
  shape_geometry_from_nonindexed_positions(
    shape_segment_triangles(segment.start, segment.end, segment.half_thickness),
  )
}

///|
fn polyline_to_mesh2d_geometry(polyline : Polyline2d) -> @mesh.Mesh2dGeometry {
  let points = polyline.points
  if points.length() < 2 {
    return shape_geometry_from_nonindexed_positions([])
  }
  let triangles : Array[@math.Vec2] = []
  for i in 0..<(points.length() - 1) {
    let part = shape_segment_triangles(
      points[i],
      points[i + 1],
      polyline.half_thickness,
    )
    for p in part {
      triangles.push(p)
    }
  }
  shape_geometry_from_nonindexed_positions(triangles)
}

///|
fn shape_clamp_float(
  value : Float,
  min_value : Float,
  max_value : Float,
) -> Float {
  if value < min_value {
    min_value
  } else if value > max_value {
    max_value
  } else {
    value
  }
}

///|
fn arc_safe_half_angle(raw_half_angle : Float) -> Float {
  shape_clamp_float(
    shape_abs_float(raw_half_angle),
    0.0F,
    @math.pi_half() * 2.0F,
  )
}

///|
fn arc_angle_from_index(
  first_angle : Float,
  last_angle : Float,
  index : Int,
  count : Int,
) -> Float {
  if count <= 1 {
    first_angle
  } else {
    let t = Float::from_double(index.to_double()) /
      Float::from_double((count - 1).to_double())
    first_angle + (last_angle - first_angle) * t
  }
}

///|
fn circular_sector_to_mesh2d_geometry(
  sector : CircularSector,
) -> @mesh.Mesh2dGeometry {
  let radius = shape_safe_positive(shape_abs_float(sector.radius), 0.5F)
  let half_angle = arc_safe_half_angle(sector.half_angle)
  let resolution = shape_safe_segments(sector.vertices, 2)
  let first_angle = @math.pi_half() - half_angle
  let last_angle = @math.pi_half() + half_angle
  let positions : Array[@math.Vec2] = []
  let uvs : Array[@math.Vec2] = []
  let indices : Array[Int] = []
  positions.push(@math.Vec2::new(0.0F, 0.0F))
  uvs.push(@math.Vec2::new(0.5F, 0.5F))
  for i in 0.. Float {
  let half_chord = radius * @math.sin(half_angle)
  let leg = radius * radius - half_chord * half_chord
  let positive_leg = if leg < 0.0F { 0.0F } else { leg }
  let sign = if half_angle <= @math.pi_half() { 1.0F } else { -1.0F }
  sign * @math.sqrt(positive_leg)
}

///|
fn circular_segment_to_mesh2d_geometry(
  segment : CircularSegment,
) -> @mesh.Mesh2dGeometry {
  let radius = shape_safe_positive(shape_abs_float(segment.radius), 0.5F)
  let half_angle = arc_safe_half_angle(segment.half_angle)
  let resolution = shape_safe_segments(segment.vertices, 2)
  let first_angle = @math.pi_half() - half_angle
  let last_angle = @math.pi_half() + half_angle
  let midpoint_y = circular_segment_apothem(radius, half_angle)
  let positions : Array[@math.Vec2] = []
  let uvs : Array[@math.Vec2] = []
  let indices : Array[Int] = []
  positions.push(@math.Vec2::new(0.0F, midpoint_y))
  uvs.push(@math.Vec2::new(0.5F, 0.5F - 0.5F * midpoint_y / radius))
  for i in 0.. @mesh.Mesh2dGeometry {
  let r = capsule.radius
  let half = capsule.half_length
  let mut segments = capsule.segments
  if segments < 4 {
    segments = 4
  }
  let pi = @math.pi_half() * 2.0F
  // Build a convex polygon perimeter (CCW), then triangulate as an indexed fan.
  let perimeter : Array[@math.Vec2] = []
  // Top arc: theta 0..pi (includes endpoints).
  for i in 0..<(segments + 1) {
    let t = Float::from_double(i.to_double()) /
      Float::from_double(segments.to_double())
    let theta = t * pi
    // theta in [0, pi]
    let x = @math.cos(theta) * r
    let y = @math.sin(theta) * r + half
    perimeter.push(@math.Vec2::new(x, y))
  }
  // Left side down to bottom arc start.
  perimeter.push(@math.Vec2::new(-r, -half))
  // Bottom arc: theta pi..2pi (skip first point to avoid dup).
  for i in 1..<(segments + 1) {
    let t = Float::from_double(i.to_double()) /
      Float::from_double(segments.to_double())
    let theta = pi + t * pi
    let x = @math.cos(theta) * r
    let y = @math.sin(theta) * r - half
    perimeter.push(@math.Vec2::new(x, y))
  }
  // Right side back to start is implicit.
  let positions : Array[@math.Vec2] = []
  positions.push(@math.Vec2::new(0.0F, 0.0F)) // center
  for p in perimeter {
    positions.push(p)
  }
  // Simple UVs based on bounds.
  let denom_x = if r == 0.0F { 1.0F } else { 2.0F * r }
  let denom_y = if half + r == 0.0F { 1.0F } else { 2.0F * (half + r) }
  let uvs : Array[@math.Vec2] = []
  for p in positions {
    let u = p.x / denom_x + 0.5F
    let v = p.y / denom_y + 0.5F
    uvs.push(@math.Vec2::new(u, v))
  }
  let indices : Array[Int] = []
  let n = perimeter.length()
  for i in 0.. Mesh2d {
  Mesh2d::from_geometry(capsule_to_mesh2d_geometry(capsule))
}

///|
pub fn Mesh2d::from_circular_sector(sector : CircularSector) -> Mesh2d {
  Mesh2d::from_geometry(circular_sector_to_mesh2d_geometry(sector))
}

///|
pub fn Mesh2d::from_circular_segment(segment : CircularSegment) -> Mesh2d {
  Mesh2d::from_geometry(circular_segment_to_mesh2d_geometry(segment))
}

///|
pub fn Mesh2d::from_circle(circle : Circle) -> Mesh2d {
  Mesh2d::from_geometry(circle_to_mesh2d_geometry(circle))
}

///|
pub fn Mesh2d::from_ellipse(ellipse : Ellipse) -> Mesh2d {
  Mesh2d::from_geometry(ellipse_to_mesh2d_geometry(ellipse))
}

///|
pub fn Mesh2d::from_annulus(annulus : Annulus) -> Mesh2d {
  Mesh2d::from_geometry(annulus_to_mesh2d_geometry(annulus))
}

///|
pub fn Mesh2d::from_ring(ring : Ring2d) -> Mesh2d {
  Mesh2d::from_geometry(ring_to_mesh2d_geometry(ring))
}

///|
pub fn Mesh2d::from_rhombus(rhombus : Rhombus) -> Mesh2d {
  Mesh2d::from_geometry(rhombus_to_mesh2d_geometry(rhombus))
}

///|
pub fn Mesh2d::from_regular_polygon(polygon : RegularPolygon) -> Mesh2d {
  Mesh2d::from_geometry(regular_polygon_to_mesh2d_geometry(polygon))
}

///|
pub fn Mesh2d::from_triangle(triangle : Triangle2d) -> Mesh2d {
  Mesh2d::from_geometry(triangle_to_mesh2d_geometry(triangle))
}

///|
pub fn Mesh2d::from_segment(segment : Segment2d) -> Mesh2d {
  Mesh2d::from_geometry(segment_to_mesh2d_geometry(segment))
}

///|
pub fn Mesh2d::from_polyline(polyline : Polyline2d) -> Mesh2d {
  Mesh2d::from_geometry(polyline_to_mesh2d_geometry(polyline))
}

///|
pub fn Mesh2d::from_rectangle(rectangle : Rectangle) -> Mesh2d {
  Mesh2d::from_geometry(rectangle_to_mesh2d_geometry(rectangle))
}

///|
pub fn Mesh2d::from_geometry(geometry : @mesh.Mesh2dGeometry) -> Mesh2d {
  let mesh = @mesh.Mesh::from_2d_geometry(geometry)
  Mesh2d::new(mesh_asset_add(mesh))
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct GpuMesh2d {
  mesh_id : Int
}

///|
pub fn GpuMesh2d::new(mesh_id : Int) -> GpuMesh2d {
  GpuMesh2d::{ mesh_id, }
}

///|
pub struct SpriteBundle {
  sprite : Sprite
  anchor : Anchor
  transform : @math.Transform
  layers : RenderLayers
  visibility : @visibility.Visibility
  inherited_visibility : @visibility.InheritedVisibility
  view_visibility : @visibility.ViewVisibility
}

///|
pub fn SpriteBundle::new(
  sprite : Sprite,
  transform : @math.Transform,
) -> SpriteBundle {
  SpriteBundle::{
    sprite,
    anchor: Anchor::default(),
    transform,
    layers: RenderLayers::default(),
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub fn SpriteBundle::new_with_layers(
  sprite : Sprite,
  transform : @math.Transform,
  layers : RenderLayers,
) -> SpriteBundle {
  SpriteBundle::{
    sprite,
    anchor: Anchor::default(),
    transform,
    layers,
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub struct Camera2dBundle {
  camera : Camera
  projection : Projection
  camera2d : Camera2d
  transform : @math.Transform
  target : RenderTarget
  layers : RenderLayers
}

///|
pub fn Camera2dBundle::new(
  camera : Camera,
  transform : @math.Transform,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target: RenderTarget::Screen,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_projection(
  camera : Camera,
  projection : Projection,
  transform : @math.Transform,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection,
    camera2d: Camera2d::default(),
    transform,
    target: RenderTarget::Screen,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_target(
  camera : Camera,
  transform : @math.Transform,
  target : RenderTarget,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_layers(
  camera : Camera,
  transform : @math.Transform,
  target : RenderTarget,
  layers : RenderLayers,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target,
    layers,
  }
}

///|
pub struct Mesh2dBundle {
  mesh : Mesh2d
  material : ColorMaterial
  transform : @math.Transform
  layers : RenderLayers
  visibility : @visibility.Visibility
  inherited_visibility : @visibility.InheritedVisibility
  view_visibility : @visibility.ViewVisibility
}

///|
pub fn Mesh2dBundle::new(
  mesh : Mesh2d,
  material : ColorMaterial,
  transform : @math.Transform,
) -> Mesh2dBundle {
  Mesh2dBundle::{
    mesh,
    material,
    transform,
    layers: RenderLayers::default(),
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub fn Mesh2dBundle::new_with_layers(
  mesh : Mesh2d,
  material : ColorMaterial,
  transform : @math.Transform,
  layers : RenderLayers,
) -> Mesh2dBundle {
  Mesh2dBundle::{
    mesh,
    material,
    transform,
    layers,
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
priv struct SpriteInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  draw_scale : @math.Vec2
  color : Color
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  uv_min : @math.Vec2
  uv_max : @math.Vec2
  scissor : @math.Rect?
}

///|
priv struct SpriteTextureSliceInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  draw_scale : @math.Vec2
  color : Color
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  scissor : @math.Rect?
  texture_slices : @math.Vec4
  target_slices : @math.Vec4
  repeat : @math.Vec4
  atlas_rect : @math.Vec4
}

///|
priv struct UiRectInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  draw_scale : @math.Vec2
  color : Color
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  uv_min : @math.Vec2
  uv_max : @math.Vec2
  scissor : @math.Rect?
  flags : Int
  radius : UiCornerRadii
  border : UiBorderRect
}

///|
priv struct UiBoxShadowInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  // Full geometry bounds (shadow size + 6*blur), in world units.
  draw_scale : @math.Vec2
  color : Color
  // Dummy texture binding (pipeline shares sprite/ui bind group layout).
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  // Inner shadow size (node size + spread), in world units.
  size : @math.Vec2
  // Corner radii: top-left, top-right, bottom-right, bottom-left (world units).
  radius : UiCornerRadii
  // Blur radius (world units).
  blur : Float
  // Shadow samples (host shader integration steps).
  samples : Int
  scissor : @math.Rect?
}

///|
priv struct UiTextureSliceInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  draw_scale : @math.Vec2
  color : Color
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  scissor : @math.Rect?
  // Normalized texture slice dividing lines (left, top, right, bottom).
  texture_slices : @math.Vec4
  // Normalized target slice dividing lines (left, top, right, bottom).
  target_slices : @math.Vec4
  // Repeat factors (side_x, side_y, center_x, center_y).
  repeat : @math.Vec4
  // Normalized atlas rect (left, top, right, bottom).
  atlas_rect : @math.Vec4
}

///|
priv struct CameraInstance {
  entity : @core.Entity
  camera : Camera
  projection : OrthographicProjection
  transform : @math.Transform
  target : RenderTarget
  layers : RenderLayers
  computed : CameraComputed
}

///|
priv struct CameraComputed {
  target_size : @math.IVec2
  scale_factor : Float
}

///|
priv struct MeshInstance {
  entity : @core.Entity
  mesh_id : Int
  transform : @math.Transform
  color : Color
  alpha_mode : AlphaMode2d
  texture : @asset.Handle[@asset.Image]?
  uv_offset : @math.Vec2
  uv_scale : @math.Vec2
  layers : RenderLayers
}

///|
priv struct RenderTargetInfo {
  id : Int
  width : Int
  height : Int
}

///|

///|
// ECS resource marker for codegen (`mgstudio gen`).
#ecs.resource
struct RenderState {
  mut draw_enabled : Bool
  mut primary_camera : @core.Entity
  sprites : Array[SpriteInstance]
  extra_sprites : Array[SpriteInstance]
  extra_sprite_texture_slices : Array[SpriteTextureSliceInstance]
  extra_ui_rects : Array[UiRectInstance]
  extra_ui_box_shadows : Array[UiBoxShadowInstance]
  extra_ui_texture_slices : Array[UiTextureSliceInstance]
  cameras : Array[CameraInstance]
  meshes : Array[MeshInstance]
  render_targets : Array[RenderTargetInfo]
}

///|
fn RenderState::new() -> RenderState {
  RenderState::{
    draw_enabled: false,
    primary_camera: @core.Entity::placeholder(),
    sprites: [],
    extra_sprites: [],
    extra_sprite_texture_slices: [],
    extra_ui_rects: [],
    extra_ui_box_shadows: [],
    extra_ui_texture_slices: [],
    cameras: [],
    meshes: [],
    render_targets: [],
  }
}

///|
fn[W : HasRes_RenderState] render_state_ref(world : W) -> Ref[RenderState]? {
  HasRes_RenderState::get_render_state_resource(world).get_ref()
}

///|
fn[W : HasRes_RenderState + @core.ChangeTickWorld] render_state_ref_mut(
  world : W,
) -> Ref[RenderState]? {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  HasRes_RenderState::get_render_state_resource(world).get_ref_mut(tick)
}

///|
fn[W : HasRes_RenderState + @core.ChangeTickWorld] init_render_state_system(
  world : W,
) -> Unit {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let res = HasRes_RenderState::get_render_state_resource(world)
  if !res.is_present() {
    res.insert(RenderState::new(), tick)
    gizmos_reset()
  }
}

///|
fn default_uv_min() -> @math.Vec2 {
  @math.Vec2::new(0.0F, 0.0F)
}

///|
fn default_uv_max() -> @math.Vec2 {
  @math.Vec2::new(1.0F, 1.0F)
}

///|
priv struct SpriteUvInfo {
  uv_min : @math.Vec2
  uv_max : @math.Vec2
  // Pixel size of the resolved sprite region (image rect / atlas rect / sprite rect).
  // This is used to interpret `Sprite.custom_size` as a final size (Bevy-style),
  // while the host expects a scale factor.
  region_size : @math.Vec2
}

///|
fn sprite_vec2_add(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x + b.x, a.y + b.y)
}

///|
fn sprite_vec2_mul(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x * b.x, a.y * b.y)
}

///|
fn sprite_draw_scale(
  sprite : Sprite,
  transform_scale : @math.Vec2,
  region_size : @math.Vec2,
) -> @math.Vec2 {
  match sprite.custom_size {
    None => transform_scale
    Some(custom_size) => {
      // The host's sprite shader uses a fixed 128x128 quad and scales it by:
      //   host_scale * (region_pixels / 128)
      // To make `custom_size` represent a final size in world units, we convert
      // it into a scale factor relative to the region size here.
      let denom_x = if region_size.x == 0.0F { 1.0F } else { region_size.x }
      let denom_y = if region_size.y == 0.0F { 1.0F } else { region_size.y }
      let factor = @math.Vec2::new(
        custom_size.x / denom_x,
        custom_size.y / denom_y,
      )
      sprite_vec2_mul(transform_scale, factor)
    }
  }
}

///|
fn sprite_anchor_offset(
  anchor : Anchor,
  rotation : Float,
  draw_scale : @math.Vec2,
) -> @math.Vec2 {
  let a = anchor.as_vec()
  let offset_local = @math.Vec2::new(-a.x * draw_scale.x, -a.y * draw_scale.y)
  let cosv = @math.cos(rotation)
  let sinv = @math.sin(rotation)
  @math.Vec2::new(
    offset_local.x * cosv - offset_local.y * sinv,
    offset_local.x * sinv + offset_local.y * cosv,
  )
}

///|
fn swap_float_pair(a : Float, b : Float) -> (Float, Float) {
  (b, a)
}

///|
fn sprite_uv_from_sprite(sprite : Sprite) -> SpriteUvInfo? {
  let texture_id = sprite.image.id()
  let mut image_width = @asset.host_asset_texture_width(texture_id)
  let mut image_height = @asset.host_asset_texture_height(texture_id)
  if image_width <= 0 || image_height <= 0 {
    // Avoid division by zero for not-yet-loaded textures.
    image_width = 1
    image_height = 1
  }
  let image_rect = @math.Rect::new(
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(
      Float::from_double(image_width.to_double()),
      Float::from_double(image_height.to_double()),
    ),
  )
  let atlas_rect = match sprite.texture_atlas {
    Some(atlas) =>
      match @asset.asset_get_texture_atlas_layout(atlas.layout) {
        Some(layout_value) =>
          if atlas.index >= 0 && atlas.index < layout_value.textures.length() {
            Some(layout_value.textures[atlas.index].as_rect())
          } else {
            None
          }
        None => None
      }
    None => None
  }
  let texture_rect = match (atlas_rect, sprite.rect) {
    (None, None) => image_rect
    (None, Some(r)) => r
    (Some(ar), None) => ar
    (Some(ar), Some(r)) => {
      let min = sprite_vec2_add(r.min, ar.min)
      let max = sprite_vec2_add(r.max, ar.min)
      @math.Rect::new(min, max)
    }
  }
  let denom_x = Float::from_double(image_width.to_double())
  let denom_y = Float::from_double(image_height.to_double())
  if denom_x <= 0.0F || denom_y <= 0.0F {
    return None
  }
  let region_size = @math.Vec2::new(
    texture_rect.max.x - texture_rect.min.x,
    texture_rect.max.y - texture_rect.min.y,
  )
  let mut uv_min = @math.Vec2::new(
    texture_rect.min.x / denom_x,
    texture_rect.min.y / denom_y,
  )
  let mut uv_max = @math.Vec2::new(
    texture_rect.max.x / denom_x,
    texture_rect.max.y / denom_y,
  )
  if sprite.flip_x {
    let pair = swap_float_pair(uv_min.x, uv_max.x)
    uv_min = @math.Vec2::new(pair.0, uv_min.y)
    uv_max = @math.Vec2::new(pair.1, uv_max.y)
  }
  if sprite.flip_y {
    let pair = swap_float_pair(uv_min.y, uv_max.y)
    uv_min = @math.Vec2::new(uv_min.x, pair.0)
    uv_max = @math.Vec2::new(uv_max.x, pair.1)
  }
  Some(SpriteUvInfo::{ uv_min, uv_max, region_size })
}

///|
fn sprite_abs_float(value : Float) -> Float {
  if value < 0.0F {
    -value
  } else {
    value
  }
}

///|
fn clamp_stretch_value(v : Float) -> Float {
  if v < 0.001F {
    0.001F
  } else {
    v
  }
}

///|
fn min3(a : Float, b : Float, c : Float) -> Float {
  let ab = if a < b { a } else { b }
  if ab < c {
    ab
  } else {
    c
  }
}

///|
fn compute_tiled_subaxis(
  image_extent : Float,
  target_extent : Float,
  mode : SliceScaleMode,
) -> Float {
  match mode {
    SliceScaleMode::Stretch => 1.0F
    SliceScaleMode::Tile(stretch_value) => {
      let denom = image_extent * clamp_stretch_value(stretch_value)
      if denom == 0.0F {
        1.0F
      } else {
        target_extent / denom
      }
    }
  }
}

///|
fn tiled_repeat_factor(draw_scale_axis : Float, stretch_value : Float) -> Float {
  let safe_stretch = clamp_stretch_value(stretch_value)
  let draw_abs = sprite_abs_float(draw_scale_axis)
  if draw_abs < 0.000001F {
    1.0F
  } else {
    draw_abs / safe_stretch
  }
}

///|
fn apply_tiled_sprite_image_mode(
  uv_min : @math.Vec2,
  uv_max : @math.Vec2,
  draw_scale : @math.Vec2,
  image_mode : SpriteImageMode,
) -> (@math.Vec2, @math.Vec2, @math.Vec2, Bool) {
  match image_mode {
    SpriteImageMode::Tiled(tiling) => {
      let repeat_x = if tiling.tile_x {
        tiled_repeat_factor(draw_scale.x, tiling.stretch_value)
      } else {
        1.0F
      }
      let repeat_y = if tiling.tile_y {
        tiled_repeat_factor(draw_scale.y, tiling.stretch_value)
      } else {
        1.0F
      }
      let uv_span_x = uv_max.x - uv_min.x
      let uv_span_y = uv_max.y - uv_min.y
      let next_uv_max = @math.Vec2::new(
        uv_min.x + uv_span_x * repeat_x,
        uv_min.y + uv_span_y * repeat_y,
      )
      let next_draw_scale = @math.Vec2::new(
        if tiling.tile_x {
          draw_scale.x / repeat_x
        } else {
          draw_scale.x
        },
        if tiling.tile_y {
          draw_scale.y / repeat_y
        } else {
          draw_scale.y
        },
      )
      (uv_min, next_uv_max, next_draw_scale, true)
    }
    _ => (uv_min, uv_max, draw_scale, false)
  }
}

///|
fn compute_sprite_texture_slices(
  image_size : @math.Vec2,
  target_size : @math.Vec2,
  mode : SpriteImageMode,
) -> (@math.Vec4, @math.Vec4, @math.Vec4) {
  match mode {
    SpriteImageMode::Sliced(slicer) => {
      let denom_x = if image_size.x == 0.0F { 1.0F } else { image_size.x }
      let denom_y = if image_size.y == 0.0F { 1.0F } else { image_size.y }
      let tdenom_x = if target_size.x == 0.0F { 1.0F } else { target_size.x }
      let tdenom_y = if target_size.y == 0.0F { 1.0F } else { target_size.y }
      let min_coeff = min3(
        target_size.x / denom_x,
        target_size.y / denom_y,
        slicer.max_corner_scale,
      )
      let slices = @math.Vec4::new(
        slicer.border.left / denom_x,
        slicer.border.top / denom_y,
        1.0F - slicer.border.right / denom_x,
        1.0F - slicer.border.bottom / denom_y,
      )
      let border = @math.Vec4::new(
        slicer.border.left / tdenom_x * min_coeff,
        slicer.border.top / tdenom_y * min_coeff,
        1.0F - slicer.border.right / tdenom_x * min_coeff,
        1.0F - slicer.border.bottom / tdenom_y * min_coeff,
      )
      let image_side_width = denom_x * (slices.z - slices.x)
      let image_side_height = denom_y * (slices.w - slices.y)
      let target_side_width = tdenom_x * (border.z - border.x)
      let target_side_height = tdenom_y * (border.w - border.y)
      let repeat_side_x = compute_tiled_subaxis(
        image_side_width,
        target_side_width,
        slicer.sides_scale_mode,
      )
      let repeat_side_y = compute_tiled_subaxis(
        image_side_height,
        target_side_height,
        slicer.sides_scale_mode,
      )
      let repeat_center_x = compute_tiled_subaxis(
        image_side_width,
        target_side_width,
        slicer.center_scale_mode,
      )
      let repeat_center_y = compute_tiled_subaxis(
        image_side_height,
        target_side_height,
        slicer.center_scale_mode,
      )
      (
        slices,
        border,
        @math.Vec4::new(
          repeat_side_x, repeat_side_y, repeat_center_x, repeat_center_y,
        ),
      )
    }
    _ =>
      (
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(1.0F, 1.0F, 1.0F, 1.0F),
      )
  }
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_create_render_target(
  world : W,
  width : Int,
  height : Int,
  nearest : Bool,
) -> @asset.Handle[@asset.Image] {
  let id = @window.host_gpu_create_render_target(width, height, nearest)
  let rs = render_state_ref_mut(world).unwrap()
  rs.val.render_targets.push(RenderTargetInfo::{ id, width, height })
  @asset.Handle::new(id)
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Sprite + Has_Anchor + Has_RenderLayers + @math.Has_Transform + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility,
] SpriteBundle::spawn(
  self : SpriteBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Sprite::get_sprite_store(world).insert(e, self.sprite, tick)
  Has_Anchor::get_anchor_store(world).insert(e, self.anchor, tick)
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @visibility.Has_Visibility::get_visibility_store(world).insert(
    e,
    self.visibility,
    tick,
  )
  @visibility.Has_InheritedVisibility::get_inherited_visibility_store(world).insert(
    e,
    self.inherited_visibility,
    tick,
  )
  @visibility.Has_ViewVisibility::get_view_visibility_store(world).insert(
    e,
    self.view_visibility,
    tick,
  )
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  e
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Camera2d + Has_Camera + Has_Projection + Has_RenderTarget + Has_RenderLayers + @math.Has_Transform,
] Camera2dBundle::spawn(
  self : Camera2dBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Camera2d::get_camera2d_store(world).insert(e, self.camera2d, tick)
  Has_Camera::get_camera_store(world).insert(e, self.camera, tick)
  Has_Projection::get_projection_store(world).insert(e, self.projection, tick)
  Has_RenderTarget::get_render_target_store(world).insert(e, self.target, tick)
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  e
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Mesh2d + Has_ColorMaterial + Has_GpuMesh2d + Has_RenderLayers + @math.Has_Transform + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility,
] Mesh2dBundle::spawn(
  self : Mesh2dBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Mesh2d::get_mesh2d_store(world).insert(e, self.mesh, tick)
  Has_ColorMaterial::get_color_material_store(world).insert(
    e,
    self.material,
    tick,
  )
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @visibility.Has_Visibility::get_visibility_store(world).insert(
    e,
    self.visibility,
    tick,
  )
  @visibility.Has_InheritedVisibility::get_inherited_visibility_store(world).insert(
    e,
    self.inherited_visibility,
    tick,
  )
  @visibility.Has_ViewVisibility::get_view_visibility_store(world).insert(
    e,
    self.view_visibility,
    tick,
  )
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  let mesh_id = match mesh_asset_get(self.mesh.mesh) {
    Some(mesh_asset) =>
      match mesh_asset.geometry2d.encode_triangle_positions_csv() {
        Some(vertices_csv) =>
          @window.host_gpu_create_mesh_triangles(vertices_csv)
        None => {
          let geometry = mesh_asset.geometry2d
          let message = match geometry.validate() {
            Some(err) => "Mesh2dGeometry validation failed: " + err.to_string()
            None => "Mesh2dGeometry flatten failed"
          }
          @window.host_debug_string(message)
          0
        }
      }
    None => {
      @window.host_debug_string("Mesh2d spawn failed: mesh handle not found")
      0
    }
  }
  Has_GpuMesh2d::get_gpu_mesh2d_store(world).insert(
    e,
    GpuMesh2d::new(mesh_id),
    tick,
  )
  e
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_sprites(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) { rs.val.extra_sprites.clear() })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_sprite_texture_slices(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_sprite_texture_slices.clear()
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_sprite_uv(
  world : W,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  uv_min : @math.Vec2,
  uv_max : @math.Vec2,
  color : Color,
  layers : RenderLayers,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_sprites.push(SpriteInstance::{
      entity: @core.Entity::placeholder(),
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale: @math.Vec2::new(transform.scale.x, transform.scale.y),
      color,
      texture,
      layers,
      uv_min,
      uv_max,
      scissor: None,
    })
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_sprite_uv_scissor(
  world : W,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  uv_min : @math.Vec2,
  uv_max : @math.Vec2,
  color : Color,
  layers : RenderLayers,
  scissor : @math.Rect?,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_sprites.push(SpriteInstance::{
      entity: @core.Entity::placeholder(),
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale: @math.Vec2::new(transform.scale.x, transform.scale.y),
      color,
      texture,
      layers,
      uv_min,
      uv_max,
      scissor,
    })
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_ui_rects(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) { rs.val.extra_ui_rects.clear() })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_ui_box_shadows(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) { rs.val.extra_ui_box_shadows.clear() })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_ui_texture_slices(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_ui_texture_slices.clear()
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_ui_rect_uv_scissor(
  world : W,
  entity : @core.Entity,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  draw_scale : @math.Vec2,
  uv_min : @math.Vec2,
  uv_max : @math.Vec2,
  color : Color,
  layers : RenderLayers,
  scissor : @math.Rect?,
  flags : Int,
  radius : UiCornerRadii,
  border : UiBorderRect,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_ui_rects.push(UiRectInstance::{
      entity,
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale,
      color,
      texture,
      layers,
      uv_min,
      uv_max,
      scissor,
      flags,
      radius,
      border,
    })
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_ui_box_shadow_scissor(
  world : W,
  entity : @core.Entity,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  draw_scale : @math.Vec2,
  color : Color,
  layers : RenderLayers,
  scissor : @math.Rect?,
  size : @math.Vec2,
  radius : UiCornerRadii,
  blur : Float,
  samples : Int,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_ui_box_shadows.push(UiBoxShadowInstance::{
      entity,
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale,
      color,
      texture,
      layers,
      size,
      radius,
      blur,
      samples,
      scissor,
    })
  })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_ui_texture_slice_scissor(
  world : W,
  entity : @core.Entity,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  draw_scale : @math.Vec2,
  color : Color,
  layers : RenderLayers,
  scissor : @math.Rect?,
  texture_slices : @math.Vec4,
  target_slices : @math.Vec4,
  repeat : @math.Vec4,
  atlas_rect : @math.Vec4,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_ui_texture_slices.push(UiTextureSliceInstance::{
      entity,
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale,
      color,
      texture,
      layers,
      scissor,
      texture_slices,
      target_slices,
      repeat,
      atlas_rect,
    })
  })
  |> ignore
}

///|
fn render2d_find_render_target_size(
  state : RenderState,
  id : Int,
) -> RenderTargetInfo? {
  for target in state.render_targets {
    if target.id == id {
      return Some(target)
    }
  }
  None
}

///|
priv struct RenderTargetComputed {
  size : @math.IVec2
  scale_factor : Float
}

///|
fn render2d_target_info(
  state : RenderState,
  target : RenderTarget,
) -> RenderTargetComputed? {
  let mut width = @window.window_width()
  let mut height = @window.window_height()
  let mut scale_factor = @window.window_scale_factor()
  if target is Image(handle) {
    let target_id = handle.id()
    if render2d_find_render_target_size(state, target_id) is Some(info) {
      width = info.width
      height = info.height
      scale_factor = 1.0F
    } else {
      return None
    }
  }
  if width <= 0 || height <= 0 {
    return None
  }
  if scale_factor <= 0.0F {
    scale_factor = 1.0F
  }
  Some(RenderTargetComputed::{
    size: @math.IVec2::new(width, height),
    scale_factor,
  })
}

///|
fn render2d_clamp_viewport(
  viewport : Viewport,
  target_size : @math.IVec2,
) -> Viewport {
  let mut position = viewport.physical_position
  let mut size = viewport.physical_size
  if position.x < 0 || position.y < 0 {
    position = @math.IVec2::new(
      if position.x < 0 {
        0
      } else {
        position.x
      },
      if position.y < 0 {
        0
      } else {
        position.y
      },
    )
  }
  if size.x < 0 || size.y < 0 {
    size = @math.IVec2::new(
      if size.x < 0 {
        0
      } else {
        size.x
      },
      if size.y < 0 {
        0
      } else {
        size.y
      },
    )
  }
  let max_position = target_size.saturating_sub(size)
  position = position.min(max_position)
  let max_size = target_size.saturating_sub(position)
  size = size.min(max_size)
  Viewport::new(position, size)
}

///|
pub fn[W] render2d_camera_system(_world : W) -> Unit {
  // Legacy system retained for API compatibility; camera extraction computes
  // derived values each frame in `render_extract_system`.
}

///|
fn render2d_sorted_camera_indices(
  cameras : Array[CameraInstance],
) -> Array[Int] {
  let count = cameras.length()
  let indices : Array[Int] = []
  for i in 0.. Unit {
  guard render_state_ref_mut(world) is Some(rs) else { return }
  rs.val.draw_enabled = true
  rs.val.primary_camera = @core.Entity::placeholder()
  rs.val.sprites.clear()
  rs.val.cameras.clear()
  rs.val.meshes.clear()
  let camera2d_store = Has_Camera2d::get_camera2d_store(world)
  let camera_store = Has_Camera::get_camera_store(world)
  let projection_store = Has_Projection::get_projection_store(world)
  let target_store = Has_RenderTarget::get_render_target_store(world)
  let layers_store = Has_RenderLayers::get_render_layers_store(world)
  let view_visibility_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let global_store = @transform.Has_GlobalTransform::get_global_transform_store(
    world,
  )
  camera2d_store.for_each(fn(entity, _marker) {
    let target = match target_store.get(entity) {
      Some(t) => t
      None => RenderTarget::Screen
    }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let camera = match camera_store.get(entity) {
      Some(v) => v
      None => Camera::default()
    }
    let projection = match projection_store.get(entity) {
      Some(Orthographic(p)) => p
      None => OrthographicProjection::default_2d()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    let mut computed = CameraComputed::{
      target_size: @math.IVec2::new(0, 0),
      scale_factor: 1.0F,
    }
    let mut camera_value = camera
    if render2d_target_info(rs.val, target) is Some(info) {
      computed = CameraComputed::{
        target_size: info.size,
        scale_factor: info.scale_factor,
      }
      if camera_value.viewport is Some(viewport) {
        let clamped = render2d_clamp_viewport(viewport, info.size)
        camera_value = Camera::{
          viewport: Some(clamped),
          order: camera_value.order,
          clear_color: camera_value.clear_color,
        }
      }
    }
    rs.val.cameras.push(CameraInstance::{
      entity,
      camera: camera_value,
      projection,
      transform,
      target,
      layers,
      computed,
    })
  })
  if rs.val.cameras.length() > 0 {
    let order = render2d_sorted_camera_indices(rs.val.cameras)
    if order.length() > 0 {
      rs.val.primary_camera = rs.val.cameras[order[0]].entity
    }
  }
  let sprite_store = Has_Sprite::get_sprite_store(world)
  let anchor_store = Has_Anchor::get_anchor_store(world)
  let scissor_store = Has_ScissorRect::get_scissor_rect_store(world)
  sprite_store.for_each(fn(entity, sprite) {
    if view_visibility_store.get(entity) is Some(vv) && !vv.is_visible() {
      return
    }
    let anchor = match anchor_store.get(entity) {
      Some(a) => a
      None => Anchor::default()
    }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    let mut uv_min = default_uv_min()
    let mut uv_max = default_uv_max()
    let mut region_size = @math.Vec2::new(1.0F, 1.0F)
    if sprite_uv_from_sprite(sprite) is Some(info) {
      uv_min = info.uv_min
      uv_max = info.uv_max
      region_size = info.region_size
    }
    let draw_scale = sprite_draw_scale(
      sprite,
      @math.Vec2::new(transform.scale.x, transform.scale.y),
      region_size,
    )
    let anchor_offset = sprite_anchor_offset(
      anchor,
      transform.rotation_z(),
      draw_scale,
    )
    let scissor = match scissor_store.get(entity) {
      Some(v) => Some(v.rect)
      None => None
    }
    let (uv_draw_min, uv_draw_max, draw_scale_for_host, needs_repeat_sampler) = apply_tiled_sprite_image_mode(
      uv_min,
      uv_max,
      draw_scale,
      sprite.image_mode,
    )
    if needs_repeat_sampler {
      @asset.asset_set_texture_sampler(
        sprite.image,
        @asset.ImageSampler::LinearRepeat,
      )
    }
    if sprite_image_mode_uses_slices(sprite.image_mode) {
      let target_size = @math.Vec2::new(
        sprite_abs_float(draw_scale.x),
        sprite_abs_float(draw_scale.y),
      )
      let (texture_slices, target_slices, repeat) = compute_sprite_texture_slices(
        region_size,
        target_size,
        sprite.image_mode,
      )
      rs.val.extra_sprite_texture_slices.push(SpriteTextureSliceInstance::{
        entity,
        transform,
        anchor_offset,
        draw_scale,
        color: sprite.color,
        texture: sprite.image,
        layers,
        scissor,
        texture_slices,
        target_slices,
        repeat,
        atlas_rect: @math.Vec4::new(uv_min.x, uv_min.y, uv_max.x, uv_max.y),
      })
    } else {
      rs.val.sprites.push(SpriteInstance::{
        entity,
        transform,
        anchor_offset,
        draw_scale: draw_scale_for_host,
        color: sprite.color,
        texture: sprite.image,
        layers,
        uv_min: uv_draw_min,
        uv_max: uv_draw_max,
        scissor,
      })
    }
  })
  let mesh_store = Has_Mesh2d::get_mesh2d_store(world)
  let gpu_mesh_store = Has_GpuMesh2d::get_gpu_mesh2d_store(world)
  let material_store = Has_ColorMaterial::get_color_material_store(world)
  mesh_store.for_each(fn(entity, _mesh) {
    if view_visibility_store.get(entity) is Some(vv) && !vv.is_visible() {
      return
    }
    guard gpu_mesh_store.get(entity) is Some(gpu) else { return }
    guard material_store.get(entity) is Some(mat) else { return }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    rs.val.meshes.push(MeshInstance::{
      entity,
      mesh_id: gpu.mesh_id,
      transform,
      color: mat.color(),
      alpha_mode: mat.alpha_mode(),
      texture: mat.texture(),
      uv_offset: mat.uv_offset(),
      uv_scale: mat.uv_scale(),
      layers,
    })
  })
}

///|
pub fn[W] render_prepare_system(_world : W) -> Unit {

}

///|
pub fn[W] render_queue_system(_world : W) -> Unit {

}

///|
fn cmp_int(a : Int, b : Int) -> Int {
  if a < b {
    -1
  } else if a > b {
    1
  } else {
    0
  }
}

///|
fn cmp_float(a : Float, b : Float) -> Int {
  if a < b {
    -1
  } else if a > b {
    1
  } else {
    0
  }
}

///|
fn mesh_alpha_phase(alpha_mode : AlphaMode2d, alpha : Float) -> Int? {
  match alpha_mode {
    Opaque => Some(0)
    Blend => Some(if alpha >= 1.0F { 0 } else { 1 })
    Mask(cutoff) => if alpha >= cutoff { Some(0) } else { None }
  }
}

///|
fn mesh_draw_alpha(alpha_mode : AlphaMode2d, alpha : Float) -> Float? {
  match alpha_mode {
    Opaque => Some(1.0F)
    Blend => Some(alpha)
    Mask(cutoff) => if alpha >= cutoff { Some(1.0F) } else { None }
  }
}

///|
priv enum DrawKind {
  SpriteMain(Int)
  SpriteExtra(Int)
  SpriteTextureSliceExtra(Int)
  UiRectExtra(Int)
  UiBoxShadowExtra(Int)
  UiTextureSliceExtra(Int)
  Mesh(Int)
}

///|
priv struct DrawItem {
  kind : DrawKind
  z : Float
  // 0 = opaque, 1 = transparent (opaque first for stable blending).
  phase : Int
  // Rough pipeline grouping: 0 = sprite, 1 = mesh, 2 = ui.
  pipeline : Int
  // Batch key for opaque items (e.g. texture id / mesh id).
  batch : Int
  // Stable tie-break keys.
  entity_id : Int
  entity_generation : Int
  tie : Int
}

///|
fn draw_items_for_layers(
  sprites : Array[SpriteInstance],
  extra_sprites : Array[SpriteInstance],
  extra_sprite_texture_slices : Array[SpriteTextureSliceInstance],
  extra_ui_rects : Array[UiRectInstance],
  extra_ui_box_shadows : Array[UiBoxShadowInstance],
  extra_ui_texture_slices : Array[UiTextureSliceInstance],
  meshes : Array[MeshInstance],
  layers : RenderLayers,
) -> Array[DrawItem] {
  let items : Array[DrawItem] = []
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: SpriteMain(i),
      z: sprite.transform.translation.z,
      phase,
      pipeline: 0,
      batch: sprite.texture.id(),
      entity_id: sprite.entity.id,
      entity_generation: sprite.entity.generation,
      tie: i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: SpriteExtra(i),
      z: sprite.transform.translation.z,
      phase,
      pipeline: 0,
      batch: sprite.texture.id(),
      entity_id: sprite.entity.id,
      entity_generation: sprite.entity.generation,
      // Ensure extra-sprites tie-break after main sprites with the same entity key.
      tie: 1_000_000 + i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: SpriteTextureSliceExtra(i),
      z: slice.transform.translation.z,
      phase,
      pipeline: 0,
      batch: slice.texture.id(),
      entity_id: slice.entity.id,
      entity_generation: slice.entity.generation,
      tie: 1_500_000 + i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: UiRectExtra(i),
      z: rect.transform.translation.z,
      phase,
      pipeline: 2,
      batch: rect.texture.id(),
      entity_id: rect.entity.id,
      entity_generation: rect.entity.generation,
      tie: 2_000_000 + i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: UiBoxShadowExtra(i),
      z: shadow.transform.translation.z,
      phase,
      pipeline: 2,
      batch: shadow.texture.id(),
      entity_id: shadow.entity.id,
      entity_generation: shadow.entity.generation,
      tie: 2_500_000 + i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: UiTextureSliceExtra(i),
      z: slice.transform.translation.z,
      phase,
      pipeline: 2,
      batch: slice.texture.id(),
      entity_id: slice.entity.id,
      entity_generation: slice.entity.generation,
      tie: 2_000_000 + i,
    })
  }
  for i in 0.. Unit {
  guard render_state_ref_mut(world) is Some(rs) else { return }
  if !rs.val.draw_enabled {
    return
  }
  rs.val.draw_enabled = false
  gizmos_prepare()
  let cameras = rs.val.cameras
  if cameras.length() == 0 {
    gizmos_clear()
    rs.val.extra_sprites.clear()
    rs.val.extra_sprite_texture_slices.clear()
    rs.val.extra_ui_rects.clear()
    rs.val.extra_ui_box_shadows.clear()
    rs.val.extra_ui_texture_slices.clear()
    return
  }
  let sprites = rs.val.sprites
  let extra_sprites = rs.val.extra_sprites
  let extra_sprite_texture_slices = rs.val.extra_sprite_texture_slices
  let extra_ui_rects = rs.val.extra_ui_rects
  let extra_ui_box_shadows = rs.val.extra_ui_box_shadows
  let extra_ui_texture_slices = rs.val.extra_ui_texture_slices
  let meshes = rs.val.meshes
  fn int_from_f32_floor_nonneg(x : Float) -> Int {
    if x <= 0.0F {
      0
    } else {
      x.to_int()
    }
  }

  fn physical_scissor_for_camera(
    rect : @math.Rect,
    viewport_x : Int,
    viewport_y : Int,
    viewport_w : Int,
    viewport_h : Int,
    scale_factor : Float,
  ) -> (Int, Int, Int, Int) {
    let sf = if scale_factor <= 0.0F { 1.0F } else { scale_factor }
    let min_x = int_from_f32_floor_nonneg(rect.min.x * sf)
    let min_y = int_from_f32_floor_nonneg(rect.min.y * sf)
    // Match Bevy-style clipping behavior more closely: clamp the max edge with
    // floor semantics to avoid leaking a trailing row/column into adjacent UI.
    let mut max_x = int_from_f32_floor_nonneg(rect.max.x * sf)
    let mut max_y = int_from_f32_floor_nonneg(rect.max.y * sf)
    if max_x < min_x {
      max_x = min_x
    }
    if max_y < min_y {
      max_y = min_y
    }
    let mut x = viewport_x + min_x
    let mut y = viewport_y + min_y
    let mut w = max_x - min_x
    let mut h = max_y - min_y
    // Clamp to the camera viewport in physical pixels.
    if x < viewport_x {
      let dx = viewport_x - x
      x = viewport_x
      w = w - dx
    }
    if y < viewport_y {
      let dy = viewport_y - y
      y = viewport_y
      h = h - dy
    }
    let max_w = viewport_w - (x - viewport_x)
    let max_h = viewport_h - (y - viewport_y)
    if w > max_w {
      w = max_w
    }
    if h > max_h {
      h = max_h
    }
    if w < 0 {
      w = 0
    }
    if h < 0 {
      h = 0
    }
    (x, y, w, h)
  }

  for index in render2d_sorted_camera_indices(cameras) {
    let camera = cameras[index]
    let mut target_id = -1
    let target_size = camera.computed.target_size
    let target_width = target_size.x
    let target_height = target_size.y
    if camera.target is Image(handle) {
      target_id = handle.id()
    }
    if target_width <= 0 || target_height <= 0 {
      continue
    }
    let mut scale_factor = camera.computed.scale_factor
    if scale_factor <= 0.0F {
      scale_factor = 1.0F
    }
    let mut viewport_x = 0
    let mut viewport_y = 0
    let mut viewport_width = target_width
    let mut viewport_height = target_height
    let mut view_width = target_width
    let mut view_height = target_height
    if camera.camera.viewport is Some(viewport) {
      viewport_x = if viewport.physical_position.x < 0 {
        0
      } else {
        viewport.physical_position.x
      }
      viewport_y = if viewport.physical_position.y < 0 {
        0
      } else {
        viewport.physical_position.y
      }
      viewport_width = if viewport.physical_size.x < 0 {
        0
      } else {
        viewport.physical_size.x
      }
      viewport_height = if viewport.physical_size.y < 0 {
        0
      } else {
        viewport.physical_size.y
      }
      let max_width = if target_width - viewport_x < 0 {
        0
      } else {
        target_width - viewport_x
      }
      let max_height = if target_height - viewport_y < 0 {
        0
      } else {
        target_height - viewport_y
      }
      if viewport_width > max_width {
        viewport_width = max_width
      }
      if viewport_height > max_height {
        viewport_height = max_height
      }
      if viewport_width <= 0 || viewport_height <= 0 {
        continue
      }
      view_width = viewport_width
      view_height = viewport_height
    }
    let view_width_f = Float::from_double(view_width.to_double()) / scale_factor
    let view_height_f = Float::from_double(view_height.to_double()) /
      scale_factor
    let view_width_logical = view_width_f.to_int()
    let view_height_logical = view_height_f.to_int()
    if view_width_logical <= 0 || view_height_logical <= 0 {
      continue
    }
    let clear = camera.camera.clear_color
    @window.host_gpu_begin_pass(
      target_id,
      view_width_logical,
      view_height_logical,
      clear.r,
      clear.g,
      clear.b,
      clear.a,
      camera.transform.translation.x,
      camera.transform.translation.y,
      camera.transform.rotation_z(),
      camera.projection.scale,
      viewport_x,
      viewport_y,
      viewport_width,
      viewport_height,
    )
    let draw_items = draw_items_for_layers(
      sprites,
      extra_sprites,
      extra_sprite_texture_slices,
      extra_ui_rects,
      extra_ui_box_shadows,
      extra_ui_texture_slices,
      meshes,
      camera.layers,
    )
    let mut scissor_active = false
    let mut scissor_x = 0
    let mut scissor_y = 0
    let mut scissor_w = 0
    let mut scissor_h = 0
    fn apply_scissor(rect_opt : @math.Rect?) -> Unit {
      match rect_opt {
        None =>
          if scissor_active {
            @window.host_gpu_clear_scissor()
            scissor_active = false
          }
        Some(rect) => {
          let (sx, sy, sw, sh) = physical_scissor_for_camera(
            rect, viewport_x, viewport_y, viewport_width, viewport_height, scale_factor,
          )
          if !scissor_active ||
            sx != scissor_x ||
            sy != scissor_y ||
            sw != scissor_w ||
            sh != scissor_h {
            @window.host_gpu_set_scissor(sx, sy, sw, sh)
            scissor_active = true
            scissor_x = sx
            scissor_y = sy
            scissor_w = sw
            scissor_h = sh
          }
        }
      }
    }

    for item in draw_items {
      match item.kind {
        SpriteMain(i) => {
          let sprite = sprites[i]
          apply_scissor(sprite.scissor)
          let transform = sprite.transform
          let pos = @math.Vec2::new(
            transform.translation.x + sprite.anchor_offset.x,
            transform.translation.y + sprite.anchor_offset.y,
          )
          @window.host_gpu_draw_sprite_uv(
            sprite.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            sprite.draw_scale.x,
            sprite.draw_scale.y,
            sprite.color.r,
            sprite.color.g,
            sprite.color.b,
            sprite.color.a,
            sprite.uv_min.x,
            sprite.uv_min.y,
            sprite.uv_max.x,
            sprite.uv_max.y,
          )
        }
        SpriteExtra(i) => {
          let sprite = extra_sprites[i]
          apply_scissor(sprite.scissor)
          let transform = sprite.transform
          let pos = @math.Vec2::new(
            transform.translation.x + sprite.anchor_offset.x,
            transform.translation.y + sprite.anchor_offset.y,
          )
          @window.host_gpu_draw_sprite_uv(
            sprite.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            sprite.draw_scale.x,
            sprite.draw_scale.y,
            sprite.color.r,
            sprite.color.g,
            sprite.color.b,
            sprite.color.a,
            sprite.uv_min.x,
            sprite.uv_min.y,
            sprite.uv_max.x,
            sprite.uv_max.y,
          )
        }
        SpriteTextureSliceExtra(i) => {
          let slice = extra_sprite_texture_slices[i]
          apply_scissor(slice.scissor)
          let transform = slice.transform
          let pos = @math.Vec2::new(
            transform.translation.x + slice.anchor_offset.x,
            transform.translation.y + slice.anchor_offset.y,
          )
          @window.host_gpu_draw_ui_texture_slice(
            slice.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            slice.draw_scale.x,
            slice.draw_scale.y,
            slice.color.r,
            slice.color.g,
            slice.color.b,
            slice.color.a,
            slice.texture_slices.x,
            slice.texture_slices.y,
            slice.texture_slices.z,
            slice.texture_slices.w,
            slice.target_slices.x,
            slice.target_slices.y,
            slice.target_slices.z,
            slice.target_slices.w,
            slice.repeat.x,
            slice.repeat.y,
            slice.repeat.z,
            slice.repeat.w,
            slice.atlas_rect.x,
            slice.atlas_rect.y,
            slice.atlas_rect.z,
            slice.atlas_rect.w,
          )
        }
        Mesh(i) => {
          apply_scissor(None)
          let mesh = meshes[i]
          guard mesh_draw_alpha(mesh.alpha_mode, mesh.color.a)
            is Some(draw_alpha) else {
            continue
          }
          let transform = mesh.transform
          let texture_id = match mesh.texture {
            Some(texture) => texture.id()
            None => -1
          }
          @window.host_gpu_draw_mesh(
            mesh.mesh_id,
            transform.translation.x,
            transform.translation.y,
            transform.rotation_z(),
            transform.scale.x,
            transform.scale.y,
            mesh.color.r,
            mesh.color.g,
            mesh.color.b,
            draw_alpha,
            texture_id,
            mesh.uv_offset.x,
            mesh.uv_offset.y,
            mesh.uv_scale.x,
            mesh.uv_scale.y,
          )
        }
        UiRectExtra(i) => {
          let rect = extra_ui_rects[i]
          apply_scissor(rect.scissor)
          let transform = rect.transform
          let pos = @math.Vec2::new(
            transform.translation.x + rect.anchor_offset.x,
            transform.translation.y + rect.anchor_offset.y,
          )
          @window.host_gpu_draw_ui_rect(
            rect.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            rect.draw_scale.x,
            rect.draw_scale.y,
            rect.color.r,
            rect.color.g,
            rect.color.b,
            rect.color.a,
            rect.uv_min.x,
            rect.uv_min.y,
            rect.uv_max.x,
            rect.uv_max.y,
            rect.flags,
            rect.radius.top_left,
            rect.radius.top_right,
            rect.radius.bottom_right,
            rect.radius.bottom_left,
            rect.border.left,
            rect.border.top,
            rect.border.right,
            rect.border.bottom,
          )
        }
        UiBoxShadowExtra(i) => {
          let shadow = extra_ui_box_shadows[i]
          apply_scissor(shadow.scissor)
          let transform = shadow.transform
          let pos = @math.Vec2::new(
            transform.translation.x + shadow.anchor_offset.x,
            transform.translation.y + shadow.anchor_offset.y,
          )
          @window.host_gpu_draw_ui_box_shadow(
            shadow.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            shadow.draw_scale.x,
            shadow.draw_scale.y,
            shadow.color.r,
            shadow.color.g,
            shadow.color.b,
            shadow.color.a,
            shadow.size.x,
            shadow.size.y,
            shadow.radius.top_left,
            shadow.radius.top_right,
            shadow.radius.bottom_right,
            shadow.radius.bottom_left,
            shadow.blur,
            shadow.samples,
          )
        }
        UiTextureSliceExtra(i) => {
          let slice = extra_ui_texture_slices[i]
          apply_scissor(slice.scissor)
          let transform = slice.transform
          let pos = @math.Vec2::new(
            transform.translation.x + slice.anchor_offset.x,
            transform.translation.y + slice.anchor_offset.y,
          )
          @window.host_gpu_draw_ui_texture_slice(
            slice.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            slice.draw_scale.x,
            slice.draw_scale.y,
            slice.color.r,
            slice.color.g,
            slice.color.b,
            slice.color.a,
            slice.texture_slices.x,
            slice.texture_slices.y,
            slice.texture_slices.z,
            slice.texture_slices.w,
            slice.target_slices.x,
            slice.target_slices.y,
            slice.target_slices.z,
            slice.target_slices.w,
            slice.repeat.x,
            slice.repeat.y,
            slice.repeat.z,
            slice.repeat.w,
            slice.atlas_rect.x,
            slice.atlas_rect.y,
            slice.atlas_rect.z,
            slice.atlas_rect.w,
          )
        }
      }
    }
    gizmos_draw_lines(camera.layers)
    @window.host_gpu_end_pass()
  }
  gizmos_clear()
  rs.val.extra_sprites.clear()
  rs.val.extra_sprite_texture_slices.clear()
  rs.val.extra_ui_rects.clear()
  rs.val.extra_ui_box_shadows.clear()
  rs.val.extra_ui_texture_slices.clear()
}

///|
pub fn[W : HasRes_RenderState] render2d_viewport_to_world_2d(
  world : W,
  screen : @math.Vec2,
) -> @math.Vec2? {
  guard render_state_ref(world) is Some(rs) else { return None }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return None
  }
  let mut found : CameraInstance? = None
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      found = Some(camera)
      break
    }
  }
  guard found is Some(cam) else { return None }
  render2d_camera_viewport_to_world_2d(
    screen,
    cam.camera,
    cam.projection,
    cam.transform,
    cam.computed.target_size,
    cam.computed.scale_factor,
  )
}

///|
pub fn[W : HasRes_RenderState] render2d_world_to_viewport(
  world_ : W,
  world : @math.Vec2,
) -> @math.Vec2? {
  guard render_state_ref(world_) is Some(rs) else { return None }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return None
  }
  let mut found : CameraInstance? = None
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      found = Some(camera)
      break
    }
  }
  guard found is Some(cam) else { return None }
  render2d_camera_world_to_viewport_2d(
    world,
    cam.camera,
    cam.projection,
    cam.transform,
    cam.computed.target_size,
    cam.computed.scale_factor,
  )
}

///|
/// Shared math for camera viewport conversions. `screen` is in logical pixels.
pub fn render2d_camera_viewport_to_world_2d(
  screen : @math.Vec2,
  camera : Camera,
  projection : OrthographicProjection,
  camera_transform : @math.Transform,
  target_size : @math.IVec2,
  scale_factor : Float,
) -> @math.Vec2? {
  let width = target_size.x
  let height = target_size.y
  if width <= 0 || height <= 0 {
    return None
  }
  let mut safe_scale_factor = scale_factor
  if safe_scale_factor <= 0.0F {
    safe_scale_factor = 1.0F
  }
  guard camera_viewport_rect_physical(width, height, camera.viewport)
    is Some((rect_x, rect_y, rect_width, rect_height)) else {
    return None
  }
  let rect_x_f = Float::from_double(rect_x.to_double()) / safe_scale_factor
  let rect_y_f = Float::from_double(rect_y.to_double()) / safe_scale_factor
  let rect_width_f = Float::from_double(rect_width.to_double()) /
    safe_scale_factor
  let rect_height_f = Float::from_double(rect_height.to_double()) /
    safe_scale_factor
  if rect_width_f <= 0.0F || rect_height_f <= 0.0F {
    return None
  }
  let half_width = rect_width_f / 2.0F
  let half_height = rect_height_f / 2.0F
  let scale = if projection.scale == 0.0F { 1.0F } else { projection.scale }
  let local_x = screen.x - rect_x_f
  let local_y = screen.y - rect_y_f
  let view_x = (local_x - half_width) * scale
  let view_y = (half_height - local_y) * scale
  let rot_z = camera_transform.rotation_z()
  let cosv = @math.cos(rot_z)
  let sinv = @math.sin(rot_z)
  let world_x = camera_transform.translation.x + view_x * cosv + view_y * sinv
  let world_y = camera_transform.translation.y - view_x * sinv + view_y * cosv
  Some(@math.Vec2::new(world_x, world_y))
}

///|
/// Shared math for camera viewport conversions. Return value is in logical pixels.
pub fn render2d_camera_world_to_viewport_2d(
  world : @math.Vec2,
  camera : Camera,
  projection : OrthographicProjection,
  camera_transform : @math.Transform,
  target_size : @math.IVec2,
  scale_factor : Float,
) -> @math.Vec2? {
  let width = target_size.x
  let height = target_size.y
  if width <= 0 || height <= 0 {
    return None
  }
  let mut safe_scale_factor = scale_factor
  if safe_scale_factor <= 0.0F {
    safe_scale_factor = 1.0F
  }
  guard camera_viewport_rect_physical(width, height, camera.viewport)
    is Some((rect_x, rect_y, rect_width, rect_height)) else {
    return None
  }
  let rect_x_f = Float::from_double(rect_x.to_double()) / safe_scale_factor
  let rect_y_f = Float::from_double(rect_y.to_double()) / safe_scale_factor
  let rect_width_f = Float::from_double(rect_width.to_double()) /
    safe_scale_factor
  let rect_height_f = Float::from_double(rect_height.to_double()) /
    safe_scale_factor
  if rect_width_f <= 0.0F || rect_height_f <= 0.0F {
    return None
  }
  let half_width = rect_width_f / 2.0F
  let half_height = rect_height_f / 2.0F
  let dx = world.x - camera_transform.translation.x
  let dy = world.y - camera_transform.translation.y
  let rot_z = camera_transform.rotation_z()
  let cosv = @math.cos(rot_z)
  let sinv = @math.sin(rot_z)
  let view_x = dx * cosv - dy * sinv
  let view_y = dx * sinv + dy * cosv
  let safe_scale = if projection.scale == 0.0F {
    1.0F
  } else {
    projection.scale
  }
  let local_x = view_x / safe_scale + half_width
  let local_y = half_height - view_y / safe_scale
  if local_x < 0.0F ||
    local_y < 0.0F ||
    local_x > rect_width_f ||
    local_y > rect_height_f {
    return None
  }
  Some(@math.Vec2::new(local_x + rect_x_f, local_y + rect_y_f))
}

///|
fn camera_viewport_rect_physical(
  target_width : Int,
  target_height : Int,
  viewport : Viewport?,
) -> (Int, Int, Int, Int)? {
  let width = target_width
  let height = target_height
  if width <= 0 || height <= 0 {
    return None
  }
  let mut rect_x = 0
  let mut rect_y = 0
  let mut rect_width = width
  let mut rect_height = height
  if viewport is Some(vp) {
    rect_x = if vp.physical_position.x < 0 { 0 } else { vp.physical_position.x }
    rect_y = if vp.physical_position.y < 0 { 0 } else { vp.physical_position.y }
    rect_width = if vp.physical_size.x < 0 { 0 } else { vp.physical_size.x }
    rect_height = if vp.physical_size.y < 0 { 0 } else { vp.physical_size.y }
    let max_width = if width - rect_x < 0 { 0 } else { width - rect_x }
    let max_height = if height - rect_y < 0 { 0 } else { height - rect_y }
    if rect_width > max_width {
      rect_width = max_width
    }
    if rect_height > max_height {
      rect_height = max_height
    }
    if rect_width <= 0 || rect_height <= 0 {
      return None
    }
  }
  Some((rect_x, rect_y, rect_width, rect_height))
}

///|
pub fn[W : HasRes_RenderState] render2d_debug_log_primary_camera(
  world : W,
) -> Unit {
  guard render_state_ref(world) is Some(rs) else { return }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return
  }
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      let target = camera.computed.target_size
      let scale = camera.computed.scale_factor
      let proj_scale = camera.projection.scale
      let mut viewport_text = "None"
      if camera.camera.viewport is Some(viewport) {
        let pos = viewport.physical_position
        let size = viewport.physical_size
        viewport_text = "pos=(" +
          pos.x.to_string() +
          "," +
          pos.y.to_string() +
          ") size=(" +
          size.x.to_string() +
          "," +
          size.y.to_string() +
          ")"
      }
      let message = "camera_debug target=(" +
        target.x.to_string() +
        "," +
        target.y.to_string() +
        ") scale=" +
        scale.to_string() +
        " proj_scale=" +
        proj_scale.to_string() +
        " viewport=" +
        viewport_text
      @window.host_debug_string(message)
      return
    }
  }
}

///|
pub fn[
  W : HasRes_RenderState + Has_Camera2d + Has_Camera + Has_Projection + Has_RenderTarget + Has_RenderLayers + Has_Sprite + Has_ScissorRect + Has_Anchor + Has_Mesh2d + Has_ColorMaterial + Has_GpuMesh2d + @math.Has_Transform + @transform.Has_GlobalTransform + @visibility.Has_ViewVisibility + @core.ChangeTickWorld,
] render2d_plugin(
  app : @app.App[W],
) -> @app.App[W] {
  app
  .add_startup_system(init_render_state_system)
  .add_system(render_extract_system)
  .add_post_update_system(render_prepare_system)
  .add_post_update_system(render_queue_system)
  .add_post_update_system(render_execute_system)
}