feat(wire): topology payload + tag
This commit is contained in:
@@ -7,6 +7,7 @@ public enum FrameTag: UInt8 {
|
|||||||
case hands = 4
|
case hands = 4
|
||||||
case face = 5
|
case face = 5
|
||||||
case skeleton2D = 6
|
case skeleton2D = 6
|
||||||
|
case topology = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fixed-size frame header. Layout (big-endian):
|
/// Fixed-size frame header. Layout (big-endian):
|
||||||
|
|||||||
@@ -195,3 +195,52 @@ public struct FacePayload: Equatable {
|
|||||||
points = pts
|
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