// 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 TextureAtlasLayout {
size : @math.UVec2
textures : Array[@math.URect]
}
///|
pub fn TextureAtlasLayout::new_empty(
dimensions : @math.UVec2,
) -> TextureAtlasLayout {
TextureAtlasLayout::{ size: dimensions, textures: [] }
}
///|
pub fn TextureAtlasLayout::from_grid(
tile_size : @math.UVec2,
columns : Int,
rows : Int,
padding? : @math.UVec2,
offset? : @math.UVec2,
) -> TextureAtlasLayout {
let padding_value = if padding is Some(value) {
value
} else {
@math.UVec2::zero()
}
let offset_value = if offset is Some(value) {
value
} else {
@math.UVec2::zero()
}
let sprites : Array[@math.URect] = []
let mut current_padding = @math.UVec2::zero()
for y in 0.. 0 {
current_padding = @math.UVec2::new(current_padding.x, padding_value.y)
}
for x in 0.. 0 {
current_padding = @math.UVec2::new(padding_value.x, current_padding.y)
}
let rect_min_x = (tile_size.x + current_padding.x) * x + offset_value.x
let rect_min_y = (tile_size.y + current_padding.y) * y + offset_value.y
let rect_min = @math.UVec2::new(rect_min_x, rect_min_y)
let rect_max = @math.UVec2::new(
rect_min_x + tile_size.x,
rect_min_y + tile_size.y,
)
sprites.push(@math.URect::new(rect_min, rect_max))
}
}
let grid_size = @math.UVec2::new(columns, rows)
let size_x = (tile_size.x + current_padding.x) * grid_size.x -
current_padding.x
let size_y = (tile_size.y + current_padding.y) * grid_size.y -
current_padding.y
TextureAtlasLayout::{
size: @math.UVec2::new(size_x, size_y),
textures: sprites,
}
}
///|
pub fn TextureAtlasLayout::add_texture(
self : TextureAtlasLayout,
rect : @math.URect,
) -> Int {
self.textures.push(rect)
self.textures.length() - 1
}
///|
pub fn TextureAtlasLayout::len(self : TextureAtlasLayout) -> Int {
self.textures.length()
}
///|
pub fn TextureAtlasLayout::is_empty(self : TextureAtlasLayout) -> Bool {
self.textures.length() == 0
}
///|
///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct TextureAtlas {
layout : Handle[TextureAtlasLayout]
index : Int
}
///|
pub fn TextureAtlas::new(
layout : Handle[TextureAtlasLayout],
index : Int,
) -> TextureAtlas {
TextureAtlas::{ layout, index }
}
///|
pub fn TextureAtlas::with_index(
self : TextureAtlas,
index : Int,
) -> TextureAtlas {
TextureAtlas::{ ..self, index, }
}
///|
pub fn TextureAtlas::with_layout(
self : TextureAtlas,
layout : Handle[TextureAtlasLayout],
) -> TextureAtlas {
TextureAtlas::{ ..self, layout, }
}
///|
pub fn TextureAtlas::texture_rect(
self : TextureAtlas,
layouts : Assets[TextureAtlasLayout],
) -> @math.URect? {
if layouts.get(self.layout) is Some(layout_value) {
if self.index >= 0 && self.index < layout_value.textures.length() {
return Some(layout_value.textures[self.index])
}
}
None
}
///|
pub struct TextureAtlasSources {
texture_ids : Array[(Handle[Image], Int)]
}
///|
pub fn TextureAtlasSources::texture_index(
self : TextureAtlasSources,
texture : Handle[Image],
) -> Int? {
let count = self.texture_ids.length()
for i in 0.. TextureAtlas? {
if self.texture_index(texture) is Some(index) {
Some(TextureAtlas::{ layout, index })
} else {
None
}
}
///|
pub(all) enum DynamicTextureAtlasBuilderError {
FailedToAllocateSpace
}
///|
pub struct DynamicTextureAtlasBuilder {
size : @math.UVec2
padding : Int
free_rects : Array[@math.URect]
}
///|
pub fn DynamicTextureAtlasBuilder::new(
size : @math.UVec2,
padding : Int,
) -> DynamicTextureAtlasBuilder {
let padding_value = if padding < 0 { 0 } else { padding }
let rect = @math.URect::new(@math.UVec2::zero(), size)
DynamicTextureAtlasBuilder::{
size,
padding: padding_value,
free_rects: [rect],
}
}
///|
fn rect_contains(outer : @math.URect, inner : @math.URect) -> Bool {
inner.min.x >= outer.min.x &&
inner.min.y >= outer.min.y &&
inner.max.x <= outer.max.x &&
inner.max.y <= outer.max.y
}
///|
fn rect_area(rect : @math.URect) -> Int {
let size = rect.size()
size.x * size.y
}
///|
fn rect_split(
rect : @math.URect,
alloc_width : Int,
alloc_height : Int,
target : Array[@math.URect],
) -> Unit {
let size = rect.size()
let remaining_width = size.x - alloc_width
let remaining_height = size.y - alloc_height
if remaining_width < 0 || remaining_height < 0 {
return
}
if remaining_width > remaining_height {
if remaining_width > 0 {
target.push(
@math.URect::new(
@math.UVec2::new(rect.min.x + alloc_width, rect.min.y),
rect.max,
),
)
}
if remaining_height > 0 {
target.push(
@math.URect::new(
@math.UVec2::new(rect.min.x, rect.min.y + alloc_height),
@math.UVec2::new(rect.min.x + alloc_width, rect.max.y),
),
)
}
} else {
if remaining_width > 0 {
target.push(
@math.URect::new(
@math.UVec2::new(rect.min.x + alloc_width, rect.min.y),
@math.UVec2::new(rect.max.x, rect.min.y + alloc_height),
),
)
}
if remaining_height > 0 {
target.push(
@math.URect::new(
@math.UVec2::new(rect.min.x, rect.min.y + alloc_height),
rect.max,
),
)
}
}
}
///|
fn prune_free_rects(free_rects : Array[@math.URect]) -> Unit {
let mut i = 0
while i < free_rects.length() {
let current = free_rects[i]
let mut removed = false
let mut j = 0
while j < free_rects.length() {
if i != j {
let other = free_rects[j]
if rect_contains(other, current) {
let last = free_rects.length() - 1
free_rects[i] = free_rects[last]
free_rects.pop() |> ignore
removed = true
break
}
}
j = j + 1
}
if !removed {
i = i + 1
}
}
}
///|
pub fn DynamicTextureAtlasBuilder::add_texture(
self : DynamicTextureAtlasBuilder,
layout : TextureAtlasLayout,
texture_size : @math.UVec2,
) -> (Int, @math.URect)? {
let padded_width = texture_size.x + self.padding
let padded_height = texture_size.y + self.padding
let mut best_index = -1
let mut best_area = 0
let count = self.free_rects.length()
for i in 0..= padded_width && size.y >= padded_height {
let area = rect_area(rect)
if best_index < 0 || area < best_area {
best_index = i
best_area = area
}
}
}
if best_index < 0 {
return None
}
let selected = self.free_rects[best_index]
let last = self.free_rects.length() - 1
self.free_rects[best_index] = self.free_rects[last]
self.free_rects.pop() |> ignore
rect_split(selected, padded_width, padded_height, self.free_rects)
prune_free_rects(self.free_rects)
let alloc_min = selected.min
let alloc_max = @math.UVec2::new(
alloc_min.x + padded_width,
alloc_min.y + padded_height,
)
let trimmed_max = @math.UVec2::new(
alloc_max.x - self.padding,
alloc_max.y - self.padding,
)
let trimmed = @math.URect::new(alloc_min, trimmed_max)
let index = layout.add_texture(trimmed)
Some((index, trimmed))
}
///|
pub(all) enum ImageSampler {
Linear
Nearest
LinearRepeat
NearestRepeat
}
///|
pub fn host_asset_set_texture_sampler(texture_id : Int, sampler : Int) -> Unit = "mgstudio_host" "asset_set_texture_sampler"
///|
pub fn asset_set_texture_sampler(
texture : Handle[Image],
sampler : ImageSampler,
) -> Unit {
let sampler_id = match sampler {
Linear => 0
Nearest => 1
LinearRepeat => 2
NearestRepeat => 3
}
host_asset_set_texture_sampler(texture.id(), sampler_id)
}
///|
pub struct TextureAtlasBuilder {
padding : @math.UVec2
initial_size : @math.UVec2
max_size : @math.UVec2
nearest : Bool
textures : Array[Handle[Image]]
}
///|
pub fn TextureAtlasBuilder::default() -> TextureAtlasBuilder {
TextureAtlasBuilder::{
padding: @math.UVec2::zero(),
initial_size: @math.UVec2::splat(256),
max_size: @math.UVec2::splat(2048),
nearest: false,
textures: [],
}
}
///|
pub fn TextureAtlasBuilder::padding(
self : TextureAtlasBuilder,
padding : @math.UVec2,
) -> TextureAtlasBuilder {
TextureAtlasBuilder::{
padding,
initial_size: self.initial_size,
max_size: self.max_size,
nearest: self.nearest,
textures: self.textures,
}
}
///|
pub fn TextureAtlasBuilder::initial_size(
self : TextureAtlasBuilder,
size : @math.UVec2,
) -> TextureAtlasBuilder {
TextureAtlasBuilder::{
padding: self.padding,
initial_size: size,
max_size: self.max_size,
nearest: self.nearest,
textures: self.textures,
}
}
///|
pub fn TextureAtlasBuilder::max_size(
self : TextureAtlasBuilder,
size : @math.UVec2,
) -> TextureAtlasBuilder {
TextureAtlasBuilder::{
padding: self.padding,
initial_size: self.initial_size,
max_size: size,
nearest: self.nearest,
textures: self.textures,
}
}
///|
pub fn TextureAtlasBuilder::sampler_nearest(
self : TextureAtlasBuilder,
nearest : Bool,
) -> TextureAtlasBuilder {
TextureAtlasBuilder::{
padding: self.padding,
initial_size: self.initial_size,
max_size: self.max_size,
nearest,
textures: self.textures,
}
}
///|
pub fn TextureAtlasBuilder::add_texture(
self : TextureAtlasBuilder,
texture : Handle[Image],
) -> TextureAtlasBuilder {
self.textures.push(texture)
self
}
///|
let texture_atlas_layouts_state : Ref[Assets[TextureAtlasLayout]] = Ref::new(
Assets::new(),
)
///|
pub fn asset_add_texture_atlas_layout(
layout : TextureAtlasLayout,
) -> Handle[TextureAtlasLayout] {
texture_atlas_layouts_state.val.add(layout)
}
///|
pub fn asset_get_texture_atlas_layout(
handle : Handle[TextureAtlasLayout],
) -> TextureAtlasLayout? {
texture_atlas_layouts_state.val.get(handle)
}
///|
pub fn asset_set_texture_atlas_layout(
handle : Handle[TextureAtlasLayout],
layout : TextureAtlasLayout,
) -> Bool {
texture_atlas_layouts_state.val.set(handle, layout)
}
///|
fn int_clamp_max(value : Int, max : Int) -> Int {
if value > max {
max
} else {
value
}
}
///|
pub fn TextureAtlasBuilder::try_build(
self : TextureAtlasBuilder,
) -> (TextureAtlasLayout, TextureAtlasSources, Handle[Image])? {
let texture_count = self.textures.length()
if texture_count <= 0 {
return None
}
let padding_value = if self.padding.x > self.padding.y {
self.padding.x
} else {
self.padding.y
}
let mut current_size = @math.UVec2::new(
if self.initial_size.x > 0 {
self.initial_size.x
} else {
1
},
if self.initial_size.y > 0 {
self.initial_size.y
} else {
1
},
)
let max_size = @math.UVec2::new(
if self.max_size.x > 0 {
self.max_size.x
} else {
1
},
if self.max_size.y > 0 {
self.max_size.y
} else {
1
},
)
let mut attempts = 0
while attempts < 64 {
if current_size.x > max_size.x || current_size.y > max_size.y {
break
}
let allocator = DynamicTextureAtlasBuilder::new(current_size, padding_value)
let layout = TextureAtlasLayout::new_empty(current_size)
let sources = TextureAtlasSources::{ texture_ids: [] }
let rects : Array[@math.URect] = []
let mut ok = true
for i in 0.. (TextureAtlasLayout, TextureAtlasSources, Handle[Image]) {
if self.try_build() is Some(value) {
value
} else {
let layout = TextureAtlasLayout::new_empty(@math.UVec2::zero())
let sources = TextureAtlasSources::{ texture_ids: [] }
(layout, sources, Handle::new(-1))
}
}