mirror of
https://github.com/ruvnet/RuView.git
synced 2026-09-01 04:55:54 +00:00
feat: add multi-actor PoseCode scene crate
This commit is contained in:
375
v2/crates/wifi-densepose-posecode/src/parser.rs
Normal file
375
v2/crates/wifi-densepose-posecode/src/parser.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
//! Bounded line parser for the PoseCode 0.2 multi-actor extension.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::model::*;
|
||||
use crate::validate::{validate_scene, ValidationConfig, ValidationSeverity};
|
||||
|
||||
const MAX_INPUT_BYTES: usize = 1_048_576;
|
||||
const MAX_LINE_BYTES: usize = 4096;
|
||||
|
||||
pub fn parse_posecode(source: &str) -> Result<Scene> {
|
||||
if source.len() > MAX_INPUT_BYTES {
|
||||
return Err(Error::parse(0, "document exceeds 1 MiB"));
|
||||
}
|
||||
let mut scene: Option<Scene> = None;
|
||||
let mut current_actor: Option<usize> = None;
|
||||
let mut current_phase: Option<usize> = None;
|
||||
let mut elapsed_ms = 0_u64;
|
||||
|
||||
for (index, raw) in source.lines().enumerate() {
|
||||
let line_no = index + 1;
|
||||
if raw.len() > MAX_LINE_BYTES {
|
||||
return Err(Error::parse(line_no, "line exceeds 4096 bytes"));
|
||||
}
|
||||
let line = strip_comment(raw).trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if scene.is_none() {
|
||||
let rest = line
|
||||
.strip_prefix("posecode scene ")
|
||||
.ok_or_else(|| Error::parse(line_no, "expected posecode scene header"))?;
|
||||
scene = Some(Scene::new(parse_quoted(rest, line_no)?));
|
||||
continue;
|
||||
}
|
||||
let doc = scene.as_mut().expect("initialized above");
|
||||
|
||||
if let Some(value) = line.strip_prefix("source ") {
|
||||
doc.source = match value {
|
||||
"authored" => SceneSource::Authored,
|
||||
"observed_wifi_csi" => SceneSource::ObservedWifiCsi,
|
||||
"imported" => SceneSource::Imported,
|
||||
_ => return Err(Error::parse(line_no, "unknown source")),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("actor ") {
|
||||
let id = value
|
||||
.strip_suffix(':')
|
||||
.ok_or_else(|| Error::parse(line_no, "actor declaration must end with ':'"))?;
|
||||
doc.actors.push(Actor::humanoid(id.trim()));
|
||||
current_actor = Some(doc.actors.len() - 1);
|
||||
current_phase = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("step ") {
|
||||
let (name, tail) = take_quoted(value, line_no)?;
|
||||
let fields: Vec<&str> = tail.trim_end_matches(':').split_whitespace().collect();
|
||||
if fields.len() != 2 {
|
||||
return Err(Error::parse(line_no, "step requires duration and timing"));
|
||||
}
|
||||
let duration_ms = parse_duration(fields[0], line_no)?;
|
||||
let timing = parse_timing(fields[1], line_no)?;
|
||||
doc.phases.push(Phase {
|
||||
name,
|
||||
start_ms: elapsed_ms,
|
||||
duration_ms,
|
||||
timing,
|
||||
actors: Vec::new(),
|
||||
contacts: Vec::new(),
|
||||
});
|
||||
elapsed_ms = elapsed_ms
|
||||
.checked_add(duration_ms)
|
||||
.ok_or_else(|| Error::parse(line_no, "timeline overflow"))?;
|
||||
current_phase = Some(doc.phases.len() - 1);
|
||||
current_actor = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("repeat ") {
|
||||
doc.repeat = value
|
||||
.parse()
|
||||
.map_err(|_| Error::parse(line_no, "repeat must be an integer"))?;
|
||||
if doc.repeat == 0 || doc.repeat > 10_000 {
|
||||
return Err(Error::parse(line_no, "repeat must be 1 to 10000"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ai) = current_actor {
|
||||
parse_actor_field(&mut doc.actors[ai], line, line_no)?;
|
||||
continue;
|
||||
}
|
||||
if let Some(pi) = current_phase {
|
||||
parse_phase_field(&mut doc.phases[pi], line, line_no)?;
|
||||
continue;
|
||||
}
|
||||
return Err(Error::parse(line_no, "directive is outside actor or step"));
|
||||
}
|
||||
|
||||
let scene = scene.ok_or_else(|| Error::parse(0, "empty document"))?;
|
||||
if let Some(issue) = validate_scene(&scene, &ValidationConfig::default())
|
||||
.into_iter()
|
||||
.find(|i| i.severity == ValidationSeverity::Error)
|
||||
{
|
||||
return Err(Error::Validation(format!(
|
||||
"{}: {}",
|
||||
issue.path, issue.message
|
||||
)));
|
||||
}
|
||||
Ok(scene)
|
||||
}
|
||||
|
||||
fn parse_actor_field(actor: &mut Actor, line: &str, line_no: usize) -> Result<()> {
|
||||
if let Some(value) = line.strip_prefix("rig ") {
|
||||
actor.rig = value.trim().into();
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("pose start = ") {
|
||||
actor.start_pose = value.trim().into();
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("track ") {
|
||||
actor.track_id = Some(
|
||||
value
|
||||
.parse()
|
||||
.map_err(|_| Error::parse(line_no, "track must be an integer"))?,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("confidence ") {
|
||||
actor.confidence = parse_confidence(value, line_no)?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(value) = line.strip_prefix("position ") {
|
||||
actor.position = parse_vec3(value, line_no)?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::parse(line_no, "unknown actor directive"))
|
||||
}
|
||||
|
||||
fn parse_phase_field(phase: &mut Phase, line: &str, line_no: usize) -> Result<()> {
|
||||
if let Some(value) = line.strip_prefix("contact ") {
|
||||
let fields: Vec<&str> = value.split_whitespace().collect();
|
||||
if !(fields.len() == 2 || fields.len() == 3) {
|
||||
return Err(Error::parse(
|
||||
line_no,
|
||||
"contact requires two effectors and optional confidence",
|
||||
));
|
||||
}
|
||||
phase.contacts.push(Contact {
|
||||
from: parse_effector(fields[0], line_no)?,
|
||||
to: parse_effector(fields[1], line_no)?,
|
||||
confidence: if fields.len() == 3 {
|
||||
parse_confidence(fields[2], line_no)?
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
let (lhs, rhs) = line
|
||||
.split_once(':')
|
||||
.ok_or_else(|| Error::parse(line_no, "phase directive requires ':'"))?;
|
||||
let (actor_id, target) = lhs
|
||||
.trim()
|
||||
.split_once('.')
|
||||
.ok_or_else(|| Error::parse(line_no, "target must be actor qualified"))?;
|
||||
let actor = phase_actor_mut(phase, actor_id);
|
||||
match target {
|
||||
"ground-lock" => {
|
||||
actor.ground_lock = rhs
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
}
|
||||
"travel" => {
|
||||
actor.travel = Some(parse_vec3_or_xz(rhs, line_no)?);
|
||||
}
|
||||
joint => {
|
||||
let fields: Vec<&str> = rhs.split_whitespace().collect();
|
||||
if !(fields.len() == 2 || fields.len() == 3) {
|
||||
return Err(Error::parse(
|
||||
line_no,
|
||||
"joint target requires action, degrees and optional confidence",
|
||||
));
|
||||
}
|
||||
actor.joints.push(JointTarget {
|
||||
joint: joint.into(),
|
||||
action: fields[0].into(),
|
||||
degrees: fields[1]
|
||||
.parse()
|
||||
.map_err(|_| Error::parse(line_no, "degrees must be numeric"))?,
|
||||
confidence: if fields.len() == 3 {
|
||||
parse_confidence(fields[2], line_no)?
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn phase_actor_mut<'a>(phase: &'a mut Phase, id: &str) -> &'a mut ActorTargets {
|
||||
if let Some(index) = phase.actors.iter().position(|a| a.actor_id == id) {
|
||||
return &mut phase.actors[index];
|
||||
}
|
||||
phase.actors.push(ActorTargets::new(id));
|
||||
phase.actors.last_mut().expect("just pushed")
|
||||
}
|
||||
|
||||
fn parse_effector(value: &str, line: usize) -> Result<EffectorRef> {
|
||||
let (actor_id, effector) = value
|
||||
.split_once('.')
|
||||
.ok_or_else(|| Error::parse(line, "effector must be actor qualified"))?;
|
||||
Ok(EffectorRef {
|
||||
actor_id: actor_id.into(),
|
||||
effector: effector.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_vec3(value: &str, line: usize) -> Result<Vec3> {
|
||||
let f: Vec<&str> = value.split_whitespace().collect();
|
||||
if f.len() != 3 {
|
||||
return Err(Error::parse(line, "position requires x y z"));
|
||||
}
|
||||
Ok(Vec3::new(
|
||||
parse_f32(f[0], line)?,
|
||||
parse_f32(f[1], line)?,
|
||||
parse_f32(f[2], line)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_vec3_or_xz(value: &str, line: usize) -> Result<Vec3> {
|
||||
let f: Vec<&str> = value.split_whitespace().collect();
|
||||
match f.len() {
|
||||
2 => Ok(Vec3::new(
|
||||
parse_f32(f[0], line)?,
|
||||
0.0,
|
||||
parse_f32(f[1], line)?,
|
||||
)),
|
||||
3 => parse_vec3(value, line),
|
||||
_ => Err(Error::parse(line, "travel requires x z or x y z")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_f32(value: &str, line: usize) -> Result<f32> {
|
||||
let v: f32 = value
|
||||
.parse()
|
||||
.map_err(|_| Error::parse(line, "expected finite number"))?;
|
||||
if !v.is_finite() {
|
||||
return Err(Error::parse(line, "expected finite number"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn parse_confidence(value: &str, line: usize) -> Result<f32> {
|
||||
let v = parse_f32(value, line)?;
|
||||
if !(0.0..=1.0).contains(&v) {
|
||||
return Err(Error::parse(line, "confidence must be in [0, 1]"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn parse_duration(value: &str, line: usize) -> Result<u64> {
|
||||
let seconds = value
|
||||
.strip_suffix('s')
|
||||
.ok_or_else(|| Error::parse(line, "duration must end in s"))?;
|
||||
let seconds = parse_f32(seconds, line)?;
|
||||
if seconds <= 0.0 || seconds > 300.0 {
|
||||
return Err(Error::parse(line, "duration must be in (0, 300] seconds"));
|
||||
}
|
||||
Ok((seconds * 1000.0).round() as u64)
|
||||
}
|
||||
|
||||
fn parse_timing(value: &str, line: usize) -> Result<Timing> {
|
||||
match value {
|
||||
"flow" => Ok(Timing::Flow),
|
||||
"settle" => Ok(Timing::Settle),
|
||||
"drive" => Ok(Timing::Drive),
|
||||
"snap" => Ok(Timing::Snap),
|
||||
"linear" => Ok(Timing::Linear),
|
||||
"ease-in" => Ok(Timing::EaseIn),
|
||||
"ease-out" => Ok(Timing::EaseOut),
|
||||
"ease-in-out" => Ok(Timing::EaseInOut),
|
||||
_ => Err(Error::parse(line, "unknown timing")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_quoted(value: &str, line: usize) -> Result<String> {
|
||||
let (quoted, tail) = take_quoted(value, line)?;
|
||||
if !tail.trim().is_empty() {
|
||||
return Err(Error::parse(line, "unexpected text after quoted name"));
|
||||
}
|
||||
Ok(quoted)
|
||||
}
|
||||
|
||||
fn take_quoted(value: &str, line: usize) -> Result<(String, &str)> {
|
||||
let rest = value
|
||||
.strip_prefix('"')
|
||||
.ok_or_else(|| Error::parse(line, "expected quoted name"))?;
|
||||
let end = rest
|
||||
.find('"')
|
||||
.ok_or_else(|| Error::parse(line, "unterminated quoted name"))?;
|
||||
Ok((rest[..end].to_string(), &rest[end + 1..]))
|
||||
}
|
||||
|
||||
fn strip_comment(line: &str) -> &str {
|
||||
let hash = line.find('#');
|
||||
let slash = line.find("//");
|
||||
match (hash, slash) {
|
||||
(Some(a), Some(b)) => &line[..a.min(b)],
|
||||
(Some(a), None) | (None, Some(a)) => &line[..a],
|
||||
(None, None) => line,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SCENE: &str = r#"posecode scene "Assisted squat"
|
||||
source observed_wifi_csi
|
||||
actor patient:
|
||||
rig humanoid
|
||||
pose start = standing
|
||||
track 7
|
||||
confidence 0.82
|
||||
position 0 0 0
|
||||
actor therapist:
|
||||
rig humanoid
|
||||
pose start = standing
|
||||
position 1.2 0 0
|
||||
step "Lower" 1.5s flow:
|
||||
patient.knee_left: flex 95 0.8
|
||||
patient.ground-lock: feet
|
||||
therapist.shoulder_left: flex 30
|
||||
contact therapist.hand_left patient.shoulder_right 0.7
|
||||
repeat 2
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_multiple_actors_and_contacts() {
|
||||
let scene = parse_posecode(SCENE).unwrap();
|
||||
assert_eq!(scene.actors.len(), 2);
|
||||
assert_eq!(scene.phases[0].actors.len(), 2);
|
||||
assert_eq!(scene.phases[0].contacts.len(), 1);
|
||||
assert_eq!(scene.repeat, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_actor_reference() {
|
||||
let bad = "posecode scene \"x\"\nactor p1:\n rig humanoid\nstep \"x\" 1s flow:\n ghost.knee_left: flex 20";
|
||||
assert!(parse_posecode(bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_finite_number() {
|
||||
let bad = "posecode scene \"x\"\nactor p1:\n position NaN 0 0";
|
||||
assert!(parse_posecode(bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_actors() {
|
||||
let bad = "posecode scene \"x\"\nactor p1:\n rig humanoid\nactor p1:\n rig humanoid";
|
||||
assert!(parse_posecode(bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversized_document_before_parsing() {
|
||||
let bad = "x".repeat(MAX_INPUT_BYTES + 1);
|
||||
assert!(parse_posecode(&bad).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user