openpencil/crates/op-figma/examples/probe_vec.rs
Kayshen-X c2cfce221c 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.
2026-07-18 15:43:24 +08:00

242 lines
8 KiB
Rust

//! 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(","));
}
}