///|
pub enum ValidationRule {
SheetGeometry
FrameNames
Durations
AnimationTags
SliceKeyframes
CollisionBoxes
AtlasConstraints
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct RuleFinding {
rule : ValidationRule
diagnostic : Diagnostic
} derive(Debug, Eq, ToJson, FromJson)
///|
pub struct ValidationResult {
findings : Array[RuleFinding]
errors : Int
warnings : Int
passed : Bool
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn ValidationResult::has_code(
self : ValidationResult,
code : String,
) -> Bool {
for finding in self.findings {
if finding.diagnostic.code == code {
return true
}
}
false
}
///|
pub fn ValidationResult::findings_for(
self : ValidationResult,
rule : ValidationRule,
) -> Array[RuleFinding] {
self.findings.filter(fn(finding) { finding.rule == rule })
}
///|
pub fn ValidationResult::diagnostics(
self : ValidationResult,
) -> Array[Diagnostic] {
self.findings.map(fn(finding) { finding.diagnostic })
}
///|
pub fn ValidationResult::to_json_string(
self : ValidationResult,
indent? : Int = 2,
) -> String {
self.to_json().stringify(indent~)
}
///|
fn add_findings(
result : Array[RuleFinding],
rule : ValidationRule,
diagnostics : Array[Diagnostic],
) -> Array[RuleFinding] {
let output = result.copy()
for diagnostic in diagnostics {
output.push({ rule, diagnostic })
}
output
}
///|
fn rule_sheet_geometry(sheet : SpriteSheet) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
if sheet.size.w <= 0 || sheet.size.h <= 0 {
diagnostics.push({
severity: Error,
code: "invalid_sheet_size",
message: "sheet dimensions must be positive",
})
}
let bounds = { x: 0, y: 0, w: sheet.size.w, h: sheet.size.h }
for frame in sheet.frames {
if !bounds.contains_rect(frame.frame) {
diagnostics.push({
severity: Error,
code: "frame_out_of_bounds",
message: frame.filename,
})
}
if frame.frame.is_empty() {
diagnostics.push({
severity: Error,
code: "empty_frame_rect",
message: frame.filename,
})
}
}
diagnostics
}
///|
fn rule_frame_names(sheet : SpriteSheet) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for i in 0.. Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for frame in sheet.frames {
if frame.duration <= 0 {
diagnostics.push({
severity: Error,
code: "invalid_duration",
message: frame.filename,
})
}
if frame.duration > 60000 {
diagnostics.push({
severity: Warning,
code: "long_duration",
message: frame.filename,
})
}
}
diagnostics
}
///|
fn rule_animation_tags(sheet : SpriteSheet) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for tag in sheet.tags {
if tag.name == "" {
diagnostics.push({
severity: Warning,
code: "empty_tag_name",
message: "animation tag has no name",
})
}
if tag.from < 0 || tag.to < tag.from || tag.to >= sheet.frames.length() {
diagnostics.push({
severity: Error,
code: "tag_out_of_range",
message: tag.name,
})
}
}
diagnostics
}
///|
fn rule_slice_keyframes(sheet : SpriteSheet) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
let bounds = { x: 0, y: 0, w: sheet.size.w, h: sheet.size.h }
for slice in sheet.slices {
if slice.keys.length() == 0 {
diagnostics.push({
severity: Warning,
code: "empty_slice_keys",
message: slice.name,
})
}
for key in slice.keys {
if key.frame < 0 || key.frame >= sheet.frames.length() {
diagnostics.push({
severity: Error,
code: "slice_frame_out_of_range",
message: slice.name,
})
}
if !bounds.contains_rect(key.bounds) {
diagnostics.push({
severity: Warning,
code: "slice_out_of_bounds",
message: slice.name,
})
}
match key.center {
Some(center) =>
if !key.bounds.contains_rect(center) {
diagnostics.push({
severity: Warning,
code: "nine_patch_center_out_of_bounds",
message: slice.name,
})
}
None => ()
}
}
}
diagnostics
}
///|
fn rule_collision_boxes(sheet : SpriteSheet) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for frame in sheet.frames {
let bounds = { x: 0, y: 0, w: frame.source_size.w, h: frame.source_size.h }
for box in frame.boxes {
if box.rect.is_empty() {
diagnostics.push({
severity: Warning,
code: "empty_collision_box",
message: frame.filename,
})
}
if !bounds.contains_rect(box.rect) {
diagnostics.push({
severity: Warning,
code: "collision_box_out_of_bounds",
message: frame.filename,
})
}
}
}
diagnostics
}
///|
pub fn run_validation_rules(sheet : SpriteSheet) -> ValidationResult {
let mut findings : Array[RuleFinding] = []
findings = add_findings(findings, SheetGeometry, rule_sheet_geometry(sheet))
findings = add_findings(findings, FrameNames, rule_frame_names(sheet))
findings = add_findings(findings, Durations, rule_durations(sheet))
findings = add_findings(findings, AnimationTags, rule_animation_tags(sheet))
findings = add_findings(findings, SliceKeyframes, rule_slice_keyframes(sheet))
findings = add_findings(findings, CollisionBoxes, rule_collision_boxes(sheet))
let mut errors = 0
let mut warnings = 0
for finding in findings {
match finding.diagnostic.severity {
Error => errors += 1
Warning => warnings += 1
}
}
{ findings, errors, warnings, passed: errors == 0 }
}
///|
pub fn validate_with_rules(
sheet : SpriteSheet,
rules : Array[ValidationRule],
) -> ValidationResult {
let full = run_validation_rules(sheet)
let findings = full.findings.filter(fn(finding) {
rules.contains(finding.rule)
})
let mut errors = 0
let mut warnings = 0
for finding in findings {
match finding.diagnostic.severity {
Error => errors += 1
Warning => warnings += 1
}
}
{ findings, errors, warnings, passed: errors == 0 }
}
///|
pub fn validation_rule_names() -> Array[String] {
[
"sheet_geometry", "frame_names", "durations", "animation_tags", "slice_keyframes",
"collision_boxes", "atlas_constraints",
]
}
///|
pub fn validation_result_summary(result : ValidationResult) -> String {
if result.passed {
"passed: {result.warnings} warnings"
} else {
"failed: {result.errors} errors, {result.warnings} warnings"
}
}
///|
pub fn validation_is_clean(result : ValidationResult) -> Bool {
result.errors == 0 && result.warnings == 0
}
///|
pub fn validation_codes(result : ValidationResult) -> Array[String] {
result.findings.map(fn(finding) { finding.diagnostic.code })
}
///|
pub fn validation_error_codes(result : ValidationResult) -> Array[String] {
result.findings.filter_map(fn(finding) {
match finding.diagnostic.severity {
Error => Some(finding.diagnostic.code)
Warning => None
}
})
}
///|
pub fn validation_warning_codes(result : ValidationResult) -> Array[String] {
result.findings.filter_map(fn(finding) {
match finding.diagnostic.severity {
Error => None
Warning => Some(finding.diagnostic.code)
}
})
}