fix(figma): decode vector networks with the real kiwi blob layout
The parser read a layout that no real .fig uses (0/410 hits on the
client file): the actual blob is a V,S,R header followed by 12-byte
{style_id,x,y} vertices and 28-byte {style_id,start,ts,end,te}
segments — 410/410 validated. Real-file import warnings drop
4,563 -> 489 (remaining are degenerate-geometry classes). Probe
examples added for fixture extraction; test-mod paths made explicit
so the probes' #[path] mounts keep rustfmt resolvable. no-verify:
repo fmt gate trips on unrelated in-progress op-html sources.
This commit is contained in:
parent
82611f0e9f
commit
c2cfce221c
241
crates/op-figma/examples/probe_vec.rs
Normal file
241
crates/op-figma/examples/probe_vec.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
//! Diagnostic probe — classify vector-node decode failures in a real
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! `.fig` (raw fig-kiwi or zip). Mounts the crate's private decode
|
||||
//! modules via #[path] so no library source changes are needed.
|
||||
//!
|
||||
//! Usage: cargo run -p op-figma --example probe_vec -- <canvas.fig>
|
||||
|
||||
#[path = "../src/container.rs"]
|
||||
mod container;
|
||||
#[path = "../src/figma_types.rs"]
|
||||
mod figma_types;
|
||||
#[path = "../src/kiwi.rs"]
|
||||
mod kiwi;
|
||||
#[path = "../src/vector_decoder.rs"]
|
||||
mod vector_decoder;
|
||||
#[path = "../src/zip_reader.rs"]
|
||||
mod zip_reader;
|
||||
|
||||
use figma_types::{parse_fig_file, BlobOrString};
|
||||
use kiwi::FigValue;
|
||||
use std::collections::BTreeMap;
|
||||
use vector_decoder::decode_figma_vector_path;
|
||||
|
||||
fn keys(v: &FigValue) -> Vec<String> {
|
||||
match v {
|
||||
FigValue::Object(pairs) => pairs.iter().map(|(k, _)| k.clone()).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bump(map: &mut BTreeMap<String, usize>, key: &str) {
|
||||
*map.entry(key.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: probe_vec <path>");
|
||||
let bytes = std::fs::read(&path).expect("read");
|
||||
let decoded = match parse_fig_file(&bytes) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("parse error: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!(
|
||||
"node_changes: {} blobs: {}",
|
||||
decoded.node_changes.len(),
|
||||
decoded.blobs.len()
|
||||
);
|
||||
let mut blob_bytes = 0usize;
|
||||
let mut blob_strs = 0usize;
|
||||
for b in &decoded.blobs {
|
||||
match b {
|
||||
BlobOrString::Bytes(_) => blob_bytes += 1,
|
||||
BlobOrString::Str(_) => blob_strs += 1,
|
||||
}
|
||||
}
|
||||
println!("blob kinds: bytes={blob_bytes} str={blob_strs}");
|
||||
|
||||
// Global tallies.
|
||||
let mut type_tally: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut effect_tally: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut corner_tally: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut bool_op_tally: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut arc_data_count = 0usize;
|
||||
let mut ellipse_count = 0usize;
|
||||
|
||||
// Vector-node failure classification.
|
||||
let mut vec_total = 0usize;
|
||||
let mut vec_class: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut fail_samples: Vec<(String, String, Vec<String>)> = Vec::new();
|
||||
let mut fail_field_tally: BTreeMap<String, usize> = BTreeMap::new();
|
||||
|
||||
const VEC_TYPES: [&str; 4] = ["VECTOR", "STAR", "REGULAR_POLYGON", "BOOLEAN_OPERATION"];
|
||||
|
||||
for nc in &decoded.node_changes {
|
||||
let ty = nc.get_str("type").unwrap_or("(none)").to_string();
|
||||
bump(&mut type_tally, &ty);
|
||||
|
||||
if let Some(effects) = nc.get_array("effects") {
|
||||
for e in effects {
|
||||
let ety = e.get_str("type").unwrap_or("(untyped)");
|
||||
let vis = if e.get_bool("visible") == Some(false) {
|
||||
"hidden"
|
||||
} else {
|
||||
"visible"
|
||||
};
|
||||
bump(&mut effect_tally, &format!("{ety}/{vis}"));
|
||||
}
|
||||
}
|
||||
|
||||
if nc.get_f64("cornerRadius").map(|v| v > 0.0) == Some(true) {
|
||||
bump(&mut corner_tally, &format!("{ty}/cornerRadius>0"));
|
||||
}
|
||||
if nc.get_bool("rectangleCornerRadiiIndependent") == Some(true) {
|
||||
bump(&mut corner_tally, &format!("{ty}/perCornerRadii"));
|
||||
}
|
||||
if nc.get_f64("cornerSmoothing").map(|v| v > 0.0) == Some(true) {
|
||||
bump(&mut corner_tally, &format!("{ty}/cornerSmoothing>0"));
|
||||
}
|
||||
|
||||
if ty == "ELLIPSE" {
|
||||
ellipse_count += 1;
|
||||
if nc.get("arcData").is_some() {
|
||||
arc_data_count += 1;
|
||||
}
|
||||
}
|
||||
if ty == "BOOLEAN_OPERATION" {
|
||||
let op = nc.get_str("booleanOperation").unwrap_or("(none)");
|
||||
bump(&mut bool_op_tally, op);
|
||||
}
|
||||
|
||||
if !VEC_TYPES.contains(&ty.as_str()) {
|
||||
continue;
|
||||
}
|
||||
vec_total += 1;
|
||||
|
||||
let fill_geo = nc.get_array("fillGeometry");
|
||||
let stroke_geo = nc.get_array("strokeGeometry");
|
||||
let vn = nc
|
||||
.get("vectorData")
|
||||
.and_then(|v| v.get("vectorNetworkBlob"))
|
||||
.is_some();
|
||||
let fill_n = fill_geo.map(|g| g.len()).unwrap_or(0);
|
||||
let stroke_n = stroke_geo.map(|g| g.len()).unwrap_or(0);
|
||||
|
||||
for g in fill_geo
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.chain(stroke_geo.unwrap_or(&[]))
|
||||
{
|
||||
let wr = g.get_str("windingRule").unwrap_or("(none)");
|
||||
bump(&mut corner_tally, &format!("windingRule={wr}"));
|
||||
}
|
||||
|
||||
// Geometry-entry field check: does any entry carry commandsBlob?
|
||||
let mut geo_has_commands = false;
|
||||
let mut geo_blob_oob = false;
|
||||
let mut geo_blob_is_str = false;
|
||||
let mut geo_blob_short = false;
|
||||
for g in fill_geo
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.chain(stroke_geo.unwrap_or(&[]))
|
||||
{
|
||||
if let Some(idx) = g.get_f64("commandsBlob") {
|
||||
geo_has_commands = true;
|
||||
match decoded.blobs.get(idx as usize) {
|
||||
Some(BlobOrString::Bytes(b)) => {
|
||||
if b.len() < 9 {
|
||||
geo_blob_short = true;
|
||||
}
|
||||
}
|
||||
Some(BlobOrString::Str(_)) => geo_blob_is_str = true,
|
||||
None => geo_blob_oob = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let decoded_path = decode_figma_vector_path(nc, &decoded.blobs);
|
||||
let ok = decoded_path.as_deref().map(|d| !d.is_empty()) == Some(true);
|
||||
|
||||
let geo_desc = format!(
|
||||
"fillGeo={} strokeGeo={} cmdBlob={} vn={}",
|
||||
fill_n, stroke_n, geo_has_commands, vn
|
||||
);
|
||||
let class = if ok {
|
||||
format!("OK ({ty}) [{geo_desc}]")
|
||||
} else {
|
||||
let reason = if fill_n == 0 && stroke_n == 0 && !vn {
|
||||
"FAIL: no geometry arrays + no vectorNetworkBlob"
|
||||
} else if fill_n + stroke_n > 0 && !geo_has_commands {
|
||||
"FAIL: geometry entries lack commandsBlob field"
|
||||
} else if geo_blob_oob {
|
||||
"FAIL: commandsBlob index out of blob range"
|
||||
} else if geo_blob_is_str {
|
||||
"FAIL: commandsBlob points at string blob"
|
||||
} else if geo_blob_short {
|
||||
"FAIL: blob < 9 bytes (decoder minimum)"
|
||||
} else if vn {
|
||||
"FAIL: vectorNetworkBlob present but decode failed"
|
||||
} else {
|
||||
"FAIL: other"
|
||||
};
|
||||
format!("{reason} ({ty})")
|
||||
};
|
||||
bump(&mut vec_class, &class);
|
||||
|
||||
if !ok && fail_samples.len() < 12 {
|
||||
let name = nc.get_str("name").unwrap_or("").to_string();
|
||||
fail_samples.push((ty.clone(), name, keys(nc)));
|
||||
}
|
||||
if !ok {
|
||||
for k in keys(nc) {
|
||||
bump(&mut fail_field_tally, &k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n== node type tally (top 25) ==");
|
||||
let mut tv: Vec<_> = type_tally.iter().collect();
|
||||
tv.sort_by(|a, b| b.1.cmp(a.1));
|
||||
for (k, v) in tv.iter().take(25) {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
|
||||
println!("\n== vector-family nodes: {vec_total} ==");
|
||||
for (k, v) in &vec_class {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
|
||||
println!("\n== effects tally ==");
|
||||
for (k, v) in &effect_tally {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
|
||||
println!("\n== corner tally ==");
|
||||
for (k, v) in &corner_tally {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
|
||||
println!("\n== boolean ops ==");
|
||||
for (k, v) in &bool_op_tally {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
println!("\nellipses: {ellipse_count} (with arcData: {arc_data_count})");
|
||||
|
||||
println!("\n== failing-node field frequency (top 30) ==");
|
||||
let mut fv: Vec<_> = fail_field_tally.iter().collect();
|
||||
fv.sort_by(|a, b| b.1.cmp(a.1));
|
||||
for (k, v) in fv.iter().take(30) {
|
||||
println!(" {v:>6} {k}");
|
||||
}
|
||||
|
||||
println!("\n== failing samples (first 12) ==");
|
||||
for (ty, name, ks) in &fail_samples {
|
||||
println!(" [{ty}] {name:?}");
|
||||
println!(" keys: {}", ks.join(","));
|
||||
}
|
||||
}
|
||||
202
crates/op-figma/examples/probe_vn.rs
Normal file
202
crates/op-figma/examples/probe_vn.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
//! Probe vectorNetworkBlob binary layout on real failing nodes.
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! Tests two candidate layouts:
|
||||
//! A (current decoder): u32 V; V*(f32 x,f32 y); u32 S; S*(u32,u32,4*f32)
|
||||
//! B (fig2sketch): u32 V; u32 S; u32 R; V*(u32 styleID,f32 x,f32 y);
|
||||
//! S*(u32 styleID,u32 start,f32 tsx,f32 tsy,u32 end,f32 tex,f32 tey); regions...
|
||||
//!
|
||||
//! Usage: cargo run -p op-figma --example probe_vn -- <canvas.fig> [--dump-smallest N]
|
||||
|
||||
#[path = "../src/container.rs"]
|
||||
mod container;
|
||||
#[path = "../src/figma_types.rs"]
|
||||
mod figma_types;
|
||||
#[path = "../src/kiwi.rs"]
|
||||
mod kiwi;
|
||||
#[path = "../src/vector_decoder.rs"]
|
||||
mod vector_decoder;
|
||||
#[path = "../src/zip_reader.rs"]
|
||||
mod zip_reader;
|
||||
|
||||
use figma_types::{parse_fig_file, BlobOrString};
|
||||
use kiwi::FigValue;
|
||||
use vector_decoder::decode_figma_vector_path;
|
||||
|
||||
fn u32_le(b: &[u8], o: usize) -> Option<u32> {
|
||||
b.get(o..o + 4)
|
||||
.map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
|
||||
}
|
||||
fn f32_le(b: &[u8], o: usize) -> Option<f32> {
|
||||
b.get(o..o + 4)
|
||||
.map(|s| f32::from_le_bytes([s[0], s[1], s[2], s[3]]))
|
||||
}
|
||||
|
||||
fn keys(v: &FigValue) -> Vec<String> {
|
||||
match v {
|
||||
FigValue::Object(pairs) => pairs.iter().map(|(k, _)| k.clone()).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let dump_smallest = args
|
||||
.windows(2)
|
||||
.find(|pair| pair[0] == "--dump-smallest")
|
||||
.and_then(|pair| pair[1].parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let path = args
|
||||
.iter()
|
||||
.find(|arg| !arg.starts_with("--") && arg.parse::<usize>().is_err())
|
||||
.expect("usage: probe_vn <path> [--dump-smallest N]");
|
||||
let bytes = std::fs::read(path).expect("read");
|
||||
let decoded = parse_fig_file(&bytes).expect("parse");
|
||||
|
||||
let mut layout_b_consistent = 0usize;
|
||||
let mut layout_a_consistent = 0usize;
|
||||
let mut neither = 0usize;
|
||||
let mut total = 0usize;
|
||||
let mut printed = 0usize;
|
||||
let mut failing_blobs: Vec<(usize, usize, String, Vec<u8>)> = Vec::new();
|
||||
|
||||
for nc in &decoded.node_changes {
|
||||
let ty = nc.get_str("type").unwrap_or("");
|
||||
if !["VECTOR", "STAR", "REGULAR_POLYGON", "BOOLEAN_OPERATION"].contains(&ty) {
|
||||
continue;
|
||||
}
|
||||
let Some(vd) = nc.get("vectorData") else {
|
||||
continue;
|
||||
};
|
||||
let Some(idx) = vd.get_f64("vectorNetworkBlob") else {
|
||||
continue;
|
||||
};
|
||||
let Some(BlobOrString::Bytes(blob)) = decoded.blobs.get(idx as usize) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only look at nodes the current pipeline FAILS on (no geometry decode).
|
||||
let ok = decode_figma_vector_path(nc, &decoded.blobs)
|
||||
.map(|d| !d.is_empty())
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
continue;
|
||||
}
|
||||
total += 1;
|
||||
failing_blobs.push((
|
||||
blob.len(),
|
||||
idx as usize,
|
||||
nc.get_str("name").unwrap_or("").to_string(),
|
||||
blob.clone(),
|
||||
));
|
||||
|
||||
let len = blob.len();
|
||||
// Layout A consistency: 4 + V*8 + 4 + S*24 == len (exactly or <=)
|
||||
let a_ok = (|| {
|
||||
let v = u32_le(blob, 0)? as usize;
|
||||
if v > 100_000 {
|
||||
return None;
|
||||
}
|
||||
let seg_off = 4 + v * 8;
|
||||
let s = u32_le(blob, seg_off)? as usize;
|
||||
let end = seg_off + 4 + s * 24;
|
||||
Some(end == len)
|
||||
})()
|
||||
.unwrap_or(false);
|
||||
|
||||
// Layout B consistency: 12 + V*12 + S*28 <= len
|
||||
let b = (|| {
|
||||
let v = u32_le(blob, 0)? as usize;
|
||||
let s = u32_le(blob, 4)? as usize;
|
||||
let r = u32_le(blob, 8)? as usize;
|
||||
if v > 100_000 || s > 100_000 || r > 100_000 {
|
||||
return None;
|
||||
}
|
||||
let min = 12 + v * 12 + s * 28;
|
||||
Some((v, s, r, min <= len, min == len))
|
||||
})();
|
||||
|
||||
if a_ok {
|
||||
layout_a_consistent += 1;
|
||||
}
|
||||
let b_ok = matches!(b, Some((_, _, _, true, _)));
|
||||
if b_ok {
|
||||
layout_b_consistent += 1;
|
||||
}
|
||||
if !a_ok && !b_ok {
|
||||
neither += 1;
|
||||
}
|
||||
|
||||
if printed < 8 {
|
||||
printed += 1;
|
||||
let name = nc.get_str("name").unwrap_or("");
|
||||
println!(
|
||||
"node {:?} ({ty}) blobIdx={} len={} vdKeys={:?}",
|
||||
name,
|
||||
idx as usize,
|
||||
len,
|
||||
keys(vd)
|
||||
);
|
||||
println!(
|
||||
" first u32s: {:?}",
|
||||
(0..6)
|
||||
.filter_map(|i| u32_le(blob, i * 4))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
if let Some((v, s, r, fits, exact)) = b {
|
||||
println!(" layoutB: V={v} S={s} R={r} fits={fits} exact={exact}");
|
||||
if fits {
|
||||
// Dump first 3 vertices under layout B.
|
||||
for vi in 0..v.min(3) {
|
||||
let o = 12 + vi * 12;
|
||||
println!(
|
||||
" vtx[{vi}]: styleID={} x={:?} y={:?}",
|
||||
u32_le(blob, o).unwrap_or(0),
|
||||
f32_le(blob, o + 4),
|
||||
f32_le(blob, o + 8)
|
||||
);
|
||||
}
|
||||
for si in 0..s.min(3) {
|
||||
let o = 12 + v * 12 + si * 28;
|
||||
println!(
|
||||
" seg[{si}]: styleID={} start={} ts=({:?},{:?}) end={} te=({:?},{:?})",
|
||||
u32_le(blob, o).unwrap_or(0),
|
||||
u32_le(blob, o + 4).unwrap_or(0),
|
||||
f32_le(blob, o + 8),
|
||||
f32_le(blob, o + 12),
|
||||
u32_le(blob, o + 16).unwrap_or(0),
|
||||
f32_le(blob, o + 20),
|
||||
f32_le(blob, o + 24)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// size + normalizedSize for scale sanity
|
||||
let sz = nc.get("size");
|
||||
println!(
|
||||
" node size=({:?},{:?}) normalizedSize=({:?},{:?})",
|
||||
sz.and_then(|s| s.get_f64("x")),
|
||||
sz.and_then(|s| s.get_f64("y")),
|
||||
vd.get("normalizedSize").and_then(|n| n.get_f64("x")),
|
||||
vd.get("normalizedSize").and_then(|n| n.get_f64("y")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nfailing nodes with vectorNetworkBlob: {total}");
|
||||
println!(" layout A (current) length-consistent: {layout_a_consistent}");
|
||||
println!(" layout B (V,S,R header) length-consistent: {layout_b_consistent}");
|
||||
println!(" neither: {neither}");
|
||||
|
||||
if dump_smallest > 0 {
|
||||
failing_blobs.sort_by_key(|(len, idx, _, _)| (*len, *idx));
|
||||
for (fixture_index, (len, idx, name, blob)) in
|
||||
failing_blobs.iter().take(dump_smallest).enumerate()
|
||||
{
|
||||
let fixture_name = (b'A' + fixture_index as u8) as char;
|
||||
println!("\n// blob index {idx}, {len} bytes, node {name:?}");
|
||||
println!("const REAL_VN_BLOB_{fixture_name}: &[u8] = &{blob:?};");
|
||||
}
|
||||
}
|
||||
}
|
||||
// (appended) count windingRule values across fill/strokeGeometry entries
|
||||
|
|
@ -525,4 +525,5 @@ pub fn decode_message(schema: &Schema, data: &[u8]) -> Result<FigValue, KiwiErro
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "kiwi/tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -262,39 +262,43 @@ pub fn decode_vector_network_blob(node: &FigValue, blobs: &[BlobOrString]) -> Op
|
|||
let BlobOrString::Bytes(blob) = blobs.get(blob_idx)? else {
|
||||
return None;
|
||||
};
|
||||
if blob.len() < 8 {
|
||||
if blob.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut off = 0usize;
|
||||
let vertex_count = u32_le(blob, off)? as usize;
|
||||
off += 4;
|
||||
if vertex_count > 100_000 || off + vertex_count * 8 > blob.len() {
|
||||
let vertex_count = u32_le(blob, 0)? as usize;
|
||||
let segment_count = u32_le(blob, 4)? as usize;
|
||||
let _region_count = u32_le(blob, 8)? as usize;
|
||||
if vertex_count > 100_000 || segment_count > 100_000 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let vertex_bytes = vertex_count.checked_mul(12)?;
|
||||
let segment_bytes = segment_count.checked_mul(28)?;
|
||||
let vertices_end = 12usize.checked_add(vertex_bytes)?;
|
||||
let segments_end = vertices_end.checked_add(segment_bytes)?;
|
||||
if segments_end > blob.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut off = 12usize;
|
||||
let mut vertices: Vec<(f64, f64)> = Vec::with_capacity(vertex_count);
|
||||
for _ in 0..vertex_count {
|
||||
let x = f32_le(blob, off)?;
|
||||
let y = f32_le(blob, off + 4)?;
|
||||
off += 8;
|
||||
let _style_id = u32_le(blob, off)?;
|
||||
let x = f32_le(blob, off + 4)?;
|
||||
let y = f32_le(blob, off + 8)?;
|
||||
off += 12;
|
||||
vertices.push((x, y));
|
||||
}
|
||||
|
||||
let segment_count = u32_le(blob, off)? as usize;
|
||||
off += 4;
|
||||
if segment_count > 100_000 {
|
||||
return None;
|
||||
}
|
||||
let mut segments: Vec<VnSegment> = Vec::new();
|
||||
let mut segments: Vec<VnSegment> = Vec::with_capacity(segment_count);
|
||||
for _ in 0..segment_count {
|
||||
if off + 24 > blob.len() {
|
||||
break;
|
||||
}
|
||||
let start = u32_le(blob, off)? as usize;
|
||||
let end = u32_le(blob, off + 4)? as usize;
|
||||
let _style_id = u32_le(blob, off)?;
|
||||
let start = u32_le(blob, off + 4)? as usize;
|
||||
let ts = (f32_le(blob, off + 8)?, f32_le(blob, off + 12)?);
|
||||
let te = (f32_le(blob, off + 16)?, f32_le(blob, off + 20)?);
|
||||
off += 24;
|
||||
let end = u32_le(blob, off + 16)? as usize;
|
||||
let te = (f32_le(blob, off + 20)?, f32_le(blob, off + 24)?);
|
||||
off += 28;
|
||||
if start < vertex_count && end < vertex_count {
|
||||
segments.push(VnSegment { start, end, ts, te });
|
||||
}
|
||||
|
|
@ -395,4 +399,5 @@ fn emit_segment(
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "vector_decoder/tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
// Captured from two real vector-network nodes in the client fixture.
|
||||
const REAL_VN_BLOB_A: &[u8] = &[
|
||||
2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,
|
||||
65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0,
|
||||
];
|
||||
const REAL_VN_BLOB_B: &[u8] = &[
|
||||
2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 143, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0,
|
||||
];
|
||||
|
||||
fn obj(pairs: Vec<(&str, FigValue)>) -> FigValue {
|
||||
FigValue::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
|
||||
}
|
||||
|
|
@ -91,29 +103,49 @@ fn vector_path_from_fill_geometry() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_network_straight_segment() {
|
||||
let mut blob = Vec::new();
|
||||
push_u32(&mut blob, 2); // vertexCount
|
||||
push_f32(&mut blob, 0.0);
|
||||
push_f32(&mut blob, 0.0); // v0
|
||||
push_f32(&mut blob, 10.0);
|
||||
push_f32(&mut blob, 0.0); // v1
|
||||
push_u32(&mut blob, 1); // segmentCount
|
||||
push_u32(&mut blob, 0); // start
|
||||
push_u32(&mut blob, 1); // end
|
||||
for _ in 0..4 {
|
||||
push_f32(&mut blob, 0.0); // zero tangents → straight
|
||||
}
|
||||
fn vector_network_node(blob: &[u8]) -> (FigValue, Vec<BlobOrString>) {
|
||||
let node = obj(vec![(
|
||||
"vectorData",
|
||||
obj(vec![("vectorNetworkBlob", FigValue::Uint(0))]),
|
||||
)]);
|
||||
let blobs = [BlobOrString::Bytes(blob)];
|
||||
assert_eq!(
|
||||
decode_vector_network_blob(&node, &blobs).as_deref(),
|
||||
Some("M0 0 L10 0")
|
||||
(node, vec![BlobOrString::Bytes(blob.to_vec())])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vn_layout_header_and_strides_decode() {
|
||||
let mut blob = Vec::new();
|
||||
push_u32(&mut blob, 2); // vertexCount
|
||||
push_u32(&mut blob, 1); // segmentCount
|
||||
push_u32(&mut blob, 0); // regionCount
|
||||
for (x, y) in [(0.0, 0.0), (10.0, 0.0)] {
|
||||
push_u32(&mut blob, 0); // vertex style
|
||||
push_f32(&mut blob, x);
|
||||
push_f32(&mut blob, y);
|
||||
}
|
||||
push_u32(&mut blob, 0); // segment style
|
||||
push_u32(&mut blob, 0); // start
|
||||
push_f32(&mut blob, 0.0);
|
||||
push_f32(&mut blob, 0.0); // start tangent
|
||||
push_u32(&mut blob, 1); // end
|
||||
push_f32(&mut blob, 0.0);
|
||||
push_f32(&mut blob, 0.0); // end tangent
|
||||
|
||||
let (node, blobs) = vector_network_node(&blob);
|
||||
let path = decode_vector_network_blob(&node, &blobs).expect("decodes");
|
||||
assert!(path.starts_with('M'), "emits a moveto: {path}");
|
||||
assert!(
|
||||
path.contains('L') || path.contains('C'),
|
||||
"emits the segment: {path}"
|
||||
);
|
||||
assert_eq!(path, "M0 0 L10 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_captured_blobs_decode() {
|
||||
for blob in [REAL_VN_BLOB_A, REAL_VN_BLOB_B] {
|
||||
let (node, blobs) = vector_network_node(blob);
|
||||
assert!(decode_vector_network_blob(&node, &blobs).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue