///|
fn validate_joint_shape(
joint : BvhJoint,
issues : Array[BvhValidationIssue],
) -> Unit {
if joint.name.length() == 0 {
issues.push(
BvhValidationIssue::error(
"joint-name-empty",
"joint name is empty",
path=joint.path,
),
)
}
if joint.channel_count != joint.channels.length() {
issues.push(
BvhValidationIssue::error(
"channel-count-mismatch",
"joint channel_count does not match channel array length",
path=joint.path,
),
)
}
if joint.is_root() && joint.position_channel_count() == 0 {
issues.push(
BvhValidationIssue::warning(
"root-without-position",
"root joint has no position channels; root motion will be zero",
path=joint.path,
),
)
}
if !joint.is_root() && joint.position_channel_count() > 0 {
issues.push(
BvhValidationIssue::warning(
"child-position-channels",
"non-root joint has position channels, which many game importers do not expect",
path=joint.path,
),
)
}
if joint.rotation_channel_count() == 0 && joint.children.length() > 0 {
issues.push(
BvhValidationIssue::warning(
"joint-without-rotation",
"joint has children but no rotation channels",
path=joint.path,
),
)
}
for channel in joint.channels {
if !channel.is_known() {
issues.push(
BvhValidationIssue::error(
"unknown-channel",
"unsupported channel '\{channel.to_string()}'",
path=joint.path,
),
)
}
}
for child in joint.children {
validate_joint_shape(child, issues)
}
}
///|
fn validate_channel_offsets(
joint : BvhJoint,
expected : Int,
issues : Array[BvhValidationIssue],
) -> Int {
if joint.channel_start != expected {
issues.push(
BvhValidationIssue::error(
"channel-start-gap",
"joint channel_start \{joint.channel_start} does not match expected \{expected}",
path=joint.path,
),
)
}
let mut next = expected + joint.channel_count
for child in joint.children {
next = validate_channel_offsets(child, next, issues)
}
next
}
///|
fn validate_motion(
document : BvhDocument,
issues : Array[BvhValidationIssue],
) -> Unit {
if document.motion.frame_count <= 0 {
issues.push(
BvhValidationIssue::error(
"no-frames",
"BVH motion must declare at least one frame",
path="$.motion.frames",
),
)
}
if document.motion.frame_time <= 0.0 {
issues.push(
BvhValidationIssue::error(
"invalid-frame-time",
"Frame Time must be greater than zero",
path="$.motion.frame_time",
),
)
}
if document.motion.frame_count != document.motion.frames.length() {
issues.push(
BvhValidationIssue::error(
"frame-count-mismatch",
"declared frame count does not match parsed frame array length",
path="$.motion.frames",
),
)
}
for i in 0.. Unit {
let stats = analyze_bvh(document)
if stats.joint_count == 1 {
issues.push(
BvhValidationIssue::warning(
"single-joint-skeleton", "skeleton has only one joint; check whether children were exported",
),
)
}
if stats.max_depth > 64 {
issues.push(
BvhValidationIssue::warning(
"very-deep-skeleton", "skeleton depth is unusually high and may be difficult to import",
),
)
}
if stats.duration_seconds > 600.0 {
issues.push(
BvhValidationIssue::warning(
"long-motion", "motion is longer than ten minutes; consider splitting clips",
),
)
}
if stats.root_motion_distance == 0.0 {
issues.push(
BvhValidationIssue::info(
"static-root", "root position channels do not move across frames",
),
)
}
}
///|
pub fn validate_document(document : BvhDocument) -> BvhValidationReport {
let issues : Array[BvhValidationIssue] = []
if document.root.name.length() == 0 {
issues.push(
BvhValidationIssue::error("missing-root", "missing ROOT hierarchy"),
)
} else {
validate_joint_shape(document.root, issues)
let next = validate_channel_offsets(document.root, 0, issues)
if next != document.motion.channel_count {
issues.push(
BvhValidationIssue::error(
"motion-channel-total",
"hierarchy declares \{next} channels, motion expects \{document.motion.channel_count}",
path="$.motion.channel_count",
),
)
}
}
validate_motion(document, issues)
validate_stats(document, issues)
BvhValidationReport::new(issues)
}
///|
pub fn validate_bvh(input : String) -> BvhValidationReport {
let parsed = parse_bvh(input)
if !parsed.ok {
BvhValidationReport::new([
BvhValidationIssue::error(
parsed.error.code(),
parsed.error.message,
path="\{parsed.error.line}:\{parsed.error.column}",
),
])
} else {
validate_document(parsed.document)
}
}
///|
pub fn validation_summary(report : BvhValidationReport) -> String {
if report.ok() {
"ok: \{report.warning_count} warning(s), \{report.info_count} info"
} else {
"failed: \{report.error_count} error(s), \{report.warning_count} warning(s)"
}
}