///|
pub enum AtlasObjective {
MinimizeArea
MinimizeWidth
MinimizeHeight
MaximizeOccupancy
Balanced
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct AtlasCandidate {
strategy : AtlasStrategy
size : Size
occupancy : Double
packed_area : Int
score : Double
valid : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct AtlasRecommendation {
best : AtlasCandidate
candidates : Array[AtlasCandidate]
objective : AtlasObjective
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn atlas_strategy_name(strategy : AtlasStrategy) -> String {
match strategy {
Rows => "rows"
CompactRows => "compact_rows"
PowerOfTwo => "power_of_two"
}
}
///|
fn candidate_score(plan : AtlasPlan, objective : AtlasObjective) -> Double {
let area = (plan.size.w * plan.size.h).to_double()
match objective {
MinimizeArea => 0.0 - area
MinimizeWidth => 0.0 - plan.size.w.to_double()
MinimizeHeight => 0.0 - plan.size.h.to_double()
MaximizeOccupancy => plan.occupancy
Balanced => plan.occupancy * 1000.0 - area / 1000.0
}
}
///|
fn make_candidate(
sheet : SpriteSheet,
strategy : AtlasStrategy,
options : AtlasOptions,
objective : AtlasObjective,
) -> AtlasCandidate {
let plan = pack_strategy(sheet, strategy~, options~)
{
strategy,
size: plan.size,
occupancy: plan.occupancy,
packed_area: plan.packed_area(),
score: candidate_score(plan, objective),
valid: plan.contains_all_frames(sheet),
}
}
///|
pub fn recommend_atlas(
sheet : SpriteSheet,
max_width~ : Int,
padding~ : Int,
objective? : AtlasObjective = Balanced,
) -> AtlasRecommendation {
let options = { max_width, padding }
let candidates = [
make_candidate(sheet, Rows, options, objective),
make_candidate(sheet, CompactRows, options, objective),
make_candidate(sheet, PowerOfTwo, options, objective),
]
let mut best = candidates[0]
for candidate in candidates[1:] {
if candidate.score > best.score && candidate.valid {
best = candidate
}
}
{ best, candidates, objective }
}
///|
pub fn AtlasRecommendation::candidate_named(
self : AtlasRecommendation,
strategy : AtlasStrategy,
) -> AtlasCandidate? {
match
self.candidates.search_by(fn(candidate) { candidate.strategy == strategy }) {
Some(index) => Some(self.candidates[index])
None => None
}
}
///|
pub fn AtlasRecommendation::to_json_string(
self : AtlasRecommendation,
indent? : Int = 2,
) -> String {
self.to_json().stringify(indent~)
}
///|
pub fn AtlasRecommendation::valid_candidates(
self : AtlasRecommendation,
) -> Array[AtlasCandidate] {
self.candidates.filter(fn(candidate) { candidate.valid })
}
///|
pub fn AtlasRecommendation::occupancy_gain(
self : AtlasRecommendation,
) -> Double {
match self.candidate_named(Rows) {
None => 0.0
Some(rows) => self.best.occupancy - rows.occupancy
}
}
///|
pub fn AtlasCandidate::area(self : AtlasCandidate) -> Int {
self.size.area()
}
///|
pub fn AtlasCandidate::aspect_ratio(self : AtlasCandidate) -> Double {
if self.size.h <= 0 {
0.0
} else {
self.size.w.to_double() / self.size.h.to_double()
}
}
///|
pub fn AtlasCandidate::is_square(self : AtlasCandidate) -> Bool {
self.size.w == self.size.h
}
///|
pub fn AtlasCandidate::score_line(self : AtlasCandidate) -> String {
"\{atlas_strategy_name(self.strategy)}: score=\{self.score}, occupancy=\{self.occupancy}, size=\{self.size.w}x\{self.size.h}"
}
///|
pub fn atlas_candidates_for_widths(
sheet : SpriteSheet,
widths : Array[Int],
padding : Int,
) -> Array[AtlasCandidate] {
let result : Array[AtlasCandidate] = []
for width in widths {
if width > 0 {
result.push(
make_candidate(
sheet,
CompactRows,
{ max_width: width, padding },
Balanced,
),
)
}
}
result
}
///|
pub fn best_width_for_occupancy(
sheet : SpriteSheet,
widths : Array[Int],
padding : Int,
) -> AtlasCandidate? {
let candidates = atlas_candidates_for_widths(sheet, widths, padding)
match candidates.get(0) {
None => None
Some(first) => {
let mut best = first
for candidate in candidates[1:] {
if candidate.occupancy > best.occupancy {
best = candidate
}
}
Some(best)
}
}
}
///|
pub fn best_width_for_area(
sheet : SpriteSheet,
widths : Array[Int],
padding : Int,
) -> AtlasCandidate? {
let candidates = atlas_candidates_for_widths(sheet, widths, padding)
match candidates.get(0) {
None => None
Some(first) => {
let mut best = first
for candidate in candidates[1:] {
if candidate.area() < best.area() {
best = candidate
}
}
Some(best)
}
}
}
///|
pub fn atlas_is_power_of_two(size : Size) -> Bool {
let power = fn(value : Int) {
let mut n = 1
while n < value {
n *= 2
}
n == value
}
power(size.w) && power(size.h)
}
///|
pub fn atlas_waste(candidate : AtlasCandidate) -> Int {
candidate.area() - candidate.packed_area
}
///|
pub fn atlas_waste_ratio(candidate : AtlasCandidate) -> Double {
if candidate.area() == 0 {
0.0
} else {
atlas_waste(candidate).to_double() / candidate.area().to_double()
}
}
///|
pub fn optimize_atlas(
sheet : SpriteSheet,
objective : AtlasObjective,
) -> AtlasRecommendation {
recommend_atlas(sheet, max_width=4096, padding=1, objective~)
}
///|
pub fn optimize_for_mobile(sheet : SpriteSheet) -> AtlasRecommendation {
recommend_atlas(sheet, max_width=2048, padding=1, objective=MinimizeArea)
}
///|
pub fn optimize_for_web(sheet : SpriteSheet) -> AtlasRecommendation {
recommend_atlas(sheet, max_width=4096, padding=2, objective=MaximizeOccupancy)
}
///|
pub fn optimize_for_ui(sheet : SpriteSheet) -> AtlasRecommendation {
recommend_atlas(sheet, max_width=1024, padding=2, objective=MinimizeWidth)
}
///|
pub fn atlas_recommendation_summary(
recommendation : AtlasRecommendation,
) -> String {
"best=\{atlas_strategy_name(recommendation.best.strategy)}, size=\{recommendation.best.size.w}x\{recommendation.best.size.h}, occupancy=\{recommendation.best.occupancy}, candidates=\{recommendation.candidates.length()}"
}