feat(wire): topology payload + tag
This commit is contained in:
@@ -7,6 +7,7 @@ public enum FrameTag: UInt8 {
|
||||
case hands = 4
|
||||
case face = 5
|
||||
case skeleton2D = 6
|
||||
case topology = 7
|
||||
}
|
||||
|
||||
/// Fixed-size frame header. Layout (big-endian):
|
||||
|
||||
@@ -195,3 +195,52 @@ public struct FacePayload: Equatable {
|
||||
points = pts
|
||||
}
|
||||
}
|
||||
|
||||
/// Self-describing skeleton topology: joint names + parent indices.
|
||||
/// Sent once per connection so consumers draw bones generically
|
||||
/// (parent -> child) with no hardcoded joint mapping.
|
||||
/// Layout (big-endian): u16 jointCount; per joint [u8 nameLen + utf8];
|
||||
/// then per joint i16 parent (-1 = root).
|
||||
public struct TopologyPayload: Equatable {
|
||||
public var jointNames: [String]
|
||||
public var parents: [Int16]
|
||||
public init(jointNames: [String], parents: [Int16]) {
|
||||
self.jointNames = jointNames
|
||||
self.parents = parents
|
||||
}
|
||||
|
||||
public func encoded() -> Data {
|
||||
var d = Data()
|
||||
d.appendBE(UInt16(min(jointNames.count, 65535)))
|
||||
for name in jointNames {
|
||||
let bytes = Array(name.utf8.prefix(255))
|
||||
d.append(UInt8(bytes.count))
|
||||
d.append(contentsOf: bytes)
|
||||
}
|
||||
for p in parents { d.appendBE(UInt16(bitPattern: p)) }
|
||||
return d
|
||||
}
|
||||
|
||||
public init?(decoding data: Data) {
|
||||
let b = [UInt8](data)
|
||||
guard b.count >= 2 else { return nil }
|
||||
let n = Int(UInt16(bigEndianBytes: b[0..<2]))
|
||||
var o = 2
|
||||
var names: [String] = []
|
||||
for _ in 0..<n {
|
||||
guard o < b.count else { return nil }
|
||||
let len = Int(b[o]); o += 1
|
||||
guard o + len <= b.count else { return nil }
|
||||
names.append(String(decoding: b[o..<o+len], as: UTF8.self))
|
||||
o += len
|
||||
}
|
||||
guard o + n * 2 <= b.count else { return nil }
|
||||
var ps: [Int16] = []
|
||||
for _ in 0..<n {
|
||||
ps.append(Int16(bitPattern: UInt16(bigEndianBytes: b[o..<o+2])))
|
||||
o += 2
|
||||
}
|
||||
jointNames = names
|
||||
parents = ps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import XCTest
|
||||
@testable import AVLiveWire
|
||||
|
||||
final class TopologyPayloadTests: XCTestCase {
|
||||
func testFrameTagTopology() {
|
||||
XCTAssertEqual(FrameTag.topology.rawValue, 7)
|
||||
}
|
||||
|
||||
func testRoundTrip() {
|
||||
let names = ["root_joint", "hips_joint", "left_arm_joint"]
|
||||
let parents: [Int16] = [-1, 0, 1]
|
||||
let p = TopologyPayload(jointNames: names, parents: parents)
|
||||
let r = TopologyPayload(decoding: p.encoded())
|
||||
XCTAssertNotNil(r)
|
||||
XCTAssertEqual(r!.jointNames, names)
|
||||
XCTAssertEqual(r!.parents, parents)
|
||||
}
|
||||
|
||||
func testRejectsTruncated() {
|
||||
// Claims 3 joints but provides no body.
|
||||
let bytes: [UInt8] = [0x00, 0x03]
|
||||
XCTAssertNil(TopologyPayload(decoding: Data(bytes)))
|
||||
}
|
||||
|
||||
func testEmpty() {
|
||||
let p = TopologyPayload(jointNames: [], parents: [])
|
||||
let r = TopologyPayload(decoding: p.encoded())
|
||||
XCTAssertNotNil(r)
|
||||
XCTAssertEqual(r!.jointNames, [])
|
||||
XCTAssertEqual(r!.parents, [])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user