///|
pub(all) struct BvhSkeletonBone {
path : String
parent_path : String
name : String
depth : Int
offset : BvhVec3
offset_length : Double
channel_count : Int
has_end_site : Bool
} derive(Eq, @debug.Debug)
///|
pub fn BvhSkeletonBone::new(
path : String,
parent_path : String,
name : String,
depth : Int,
offset : BvhVec3,
channel_count : Int,
has_end_site : Bool,
) -> BvhSkeletonBone {
{
path,
parent_path,
name,
depth,
offset,
offset_length: offset.magnitude(),
channel_count,
has_end_site,
}
}
///|
fn parent_path_of(path : String) -> String {
match path.rev_find("/") {
Some(index) => path[:index].to_owned()
None => ""
}
}
///|
fn collect_bones_into(joint : BvhJoint, out : Array[BvhSkeletonBone]) -> Unit {
out.push(
BvhSkeletonBone::new(
joint.path,
parent_path_of(joint.path),
joint.name,
joint.depth,
joint.offset,
joint.channel_count,
joint.end_sites.length() > 0,
),
)
for child in joint.children {
collect_bones_into(child, out)
}
}
///|
pub fn skeleton_bones(document : BvhDocument) -> Array[BvhSkeletonBone] {
let out : Array[BvhSkeletonBone] = []
if document.root.name.length() > 0 {
collect_bones_into(document.root, out)
}
out
}
///|
fn collect_leaf_paths_into(joint : BvhJoint, out : Array[String]) -> Unit {
if joint.children.length() == 0 {
out.push(joint.path)
} else {
for child in joint.children {
collect_leaf_paths_into(child, out)
}
}
}
///|
pub fn leaf_joint_paths(document : BvhDocument) -> Array[String] {
let out : Array[String] = []
if document.root.name.length() > 0 {
collect_leaf_paths_into(document.root, out)
}
out
}
///|
fn collect_end_site_paths_into(joint : BvhJoint, out : Array[String]) -> Unit {
for end_site in joint.end_sites {
out.push(end_site.path)
}
for child in joint.children {
collect_end_site_paths_into(child, out)
}
}
///|
pub fn end_site_paths(document : BvhDocument) -> Array[String] {
let out : Array[String] = []
if document.root.name.length() > 0 {
collect_end_site_paths_into(document.root, out)
}
out
}
///|
fn string_array_contains(items : Array[String], value : String) -> Bool {
for item in items {
if item == value {
break true
}
} nobreak {
false
}
}
///|
pub fn skeleton_signature(document : BvhDocument) -> String {
let parts : Array[String] = []
for bone in skeleton_bones(document) {
parts.push(
"\{bone.path}#d\{bone.depth}#c\{bone.channel_count}#e\{bone.has_end_site}",
)
}
parts.join(";")
}
///|
pub(all) struct BvhSkeletonSummary {
root_name : String
bone_count : Int
leaf_count : Int
end_site_count : Int
max_depth : Int
total_offset_length : Double
signature : String
} derive(Eq, @debug.Debug)
///|
pub fn BvhSkeletonSummary::empty() -> BvhSkeletonSummary {
{
root_name: "",
bone_count: 0,
leaf_count: 0,
end_site_count: 0,
max_depth: 0,
total_offset_length: 0.0,
signature: "",
}
}
///|
pub fn summarize_skeleton(document : BvhDocument) -> BvhSkeletonSummary {
if document.root.name.length() == 0 {
BvhSkeletonSummary::empty()
} else {
let bones = skeleton_bones(document)
let total = for bone in bones; sum = 0.0 {
continue sum + bone.offset_length
} nobreak {
sum
}
{
root_name: document.root.name,
bone_count: bones.length(),
leaf_count: leaf_joint_paths(document).length(),
end_site_count: end_site_paths(document).length(),
max_depth: max_joint_depth(document.root),
total_offset_length: total,
signature: skeleton_signature(document),
}
}
}
///|
pub(all) struct BvhSkeletonDiff {
compatible : Bool
matched_paths : Array[String]
missing_paths : Array[String]
extra_paths : Array[String]
channel_mismatch_paths : Array[String]
offset_warning_paths : Array[String]
} derive(Eq, @debug.Debug)
///|
pub fn BvhSkeletonDiff::new(
matched_paths : Array[String],
missing_paths : Array[String],
extra_paths : Array[String],
channel_mismatch_paths : Array[String],
offset_warning_paths : Array[String],
) -> BvhSkeletonDiff {
{
compatible: missing_paths.length() == 0 &&
extra_paths.length() == 0 &&
channel_mismatch_paths.length() == 0,
matched_paths,
missing_paths,
extra_paths,
channel_mismatch_paths,
offset_warning_paths,
}
}
///|
pub fn BvhSkeletonDiff::issue_count(self : BvhSkeletonDiff) -> Int {
self.missing_paths.length() +
self.extra_paths.length() +
self.channel_mismatch_paths.length() +
self.offset_warning_paths.length()
}
///|
fn bone_by_path(
bones : Array[BvhSkeletonBone],
path : String,
) -> BvhSkeletonBone? {
for bone in bones {
if bone.path == path {
break Some(bone)
}
} nobreak {
None
}
}
///|
pub fn compare_skeletons(
reference : BvhDocument,
candidate : BvhDocument,
offset_epsilon? : Double = 0.001,
) -> BvhSkeletonDiff {
let ref_bones = skeleton_bones(reference)
let cand_bones = skeleton_bones(candidate)
let ref_paths = joint_paths(reference)
let cand_paths = joint_paths(candidate)
let matched : Array[String] = []
let missing : Array[String] = []
let extra : Array[String] = []
let channel_mismatches : Array[String] = []
let offset_warnings : Array[String] = []
for ref_bone in ref_bones {
match bone_by_path(cand_bones, ref_bone.path) {
Some(cand_bone) => {
matched.push(ref_bone.path)
if ref_bone.channel_count != cand_bone.channel_count {
channel_mismatches.push(ref_bone.path)
}
if abs_double(ref_bone.offset_length - cand_bone.offset_length) >
offset_epsilon {
offset_warnings.push(ref_bone.path)
}
}
None => missing.push(ref_bone.path)
}
}
for cand_path in cand_paths {
if !string_array_contains(ref_paths, cand_path) {
extra.push(cand_path)
}
}
BvhSkeletonDiff::new(
matched, missing, extra, channel_mismatches, offset_warnings,
)
}
///|
pub(all) struct BvhRetargetMapEntry {
source_path : String
target_path : String
source_channel_count : Int
target_channel_count : Int
channel_compatible : Bool
notes : String
} derive(Eq, @debug.Debug)
///|
pub fn BvhRetargetMapEntry::new(
source_path : String,
target_path : String,
source_channel_count : Int,
target_channel_count : Int,
) -> BvhRetargetMapEntry {
let compatible = source_channel_count == target_channel_count
{
source_path,
target_path,
source_channel_count,
target_channel_count,
channel_compatible: compatible,
notes: if compatible {
"channels match"
} else {
"channel count differs"
},
}
}
///|
fn bone_by_name(
bones : Array[BvhSkeletonBone],
name : String,
) -> BvhSkeletonBone? {
for bone in bones {
if bone.name == name {
break Some(bone)
}
} nobreak {
None
}
}
///|
pub fn build_name_based_retarget_map(
source : BvhDocument,
target : BvhDocument,
) -> Array[BvhRetargetMapEntry] {
let out : Array[BvhRetargetMapEntry] = []
let source_bones = skeleton_bones(source)
let target_bones = skeleton_bones(target)
for source_bone in source_bones {
match bone_by_name(target_bones, source_bone.name) {
Some(target_bone) =>
out.push(
BvhRetargetMapEntry::new(
source_bone.path,
target_bone.path,
source_bone.channel_count,
target_bone.channel_count,
),
)
None => ()
}
}
out
}
///|
pub fn skeleton_summary_to_json(summary : BvhSkeletonSummary) -> String {
let result =
$|{"rootName":\{json_string(summary.root_name)},"boneCount":\{summary.bone_count},"leafCount":\{summary.leaf_count},"endSiteCount":\{summary.end_site_count},"maxDepth":\{summary.max_depth},"totalOffsetLength":\{summary.total_offset_length},"signature":\{json_string(summary.signature)}}
result
}
///|
pub fn skeleton_diff_to_json(diff : BvhSkeletonDiff) -> String {
fn quoted(items : Array[String]) -> String {
let out : Array[String] = []
for item in items {
out.push(json_string(item))
}
"[" + out.join(",") + "]"
}
let result =
$|{"compatible":\{diff.compatible},"matchedPaths":\{quoted(diff.matched_paths)},"missingPaths":\{quoted(diff.missing_paths)},"extraPaths":\{quoted(diff.extra_paths)},"channelMismatchPaths":\{quoted(diff.channel_mismatch_paths)},"offsetWarningPaths":\{quoted(diff.offset_warning_paths)}}
result
}
///|
pub fn retarget_map_to_csv(entries : Array[BvhRetargetMapEntry]) -> String {
let rows : Array[String] = [
"source_path,target_path,source_channel_count,target_channel_count,channel_compatible,notes",
]
for entry in entries {
rows.push(
"\{entry.source_path},\{entry.target_path},\{entry.source_channel_count},\{entry.target_channel_count},\{entry.channel_compatible},\{entry.notes}",
)
}
rows.join("\n")
}
///|
pub fn skeleton_bones_csv(document : BvhDocument) -> String {
let rows : Array[String] = [
"path,parent_path,name,depth,offset_x,offset_y,offset_z,offset_length,channel_count,has_end_site",
]
for bone in skeleton_bones(document) {
rows.push(
"\{bone.path},\{bone.parent_path},\{bone.name},\{bone.depth},\{bone.offset.to_csv()},\{bone.offset_length},\{bone.channel_count},\{bone.has_end_site}",
)
}
rows.join("\n")
}