scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
pub(super) fn nearest_root_field(input: &str) -> Option<&'static str> {
    nearest(input, ROOT_FIELDS)
}

pub(super) fn nearest_import_field(input: &str) -> Option<&'static str> {
    nearest(input, IMPORT_FIELDS)
}

pub(super) fn nearest_capture_field(input: &str) -> Option<&'static str> {
    nearest(input, CAPTURE_FIELDS)
}

fn nearest(input: &str, candidates: &'static [&'static str]) -> Option<&'static str> {
    candidates
        .iter()
        .map(|candidate| (*candidate, edit_distance(input, candidate)))
        .filter(|(_, distance)| *distance <= 4)
        .min_by(|(left_name, left), (right_name, right)| {
            left.cmp(right).then_with(|| left_name.cmp(right_name))
        })
        .map(|(candidate, _)| candidate)
}

fn edit_distance(left: &str, right: &str) -> usize {
    let right_chars = right.chars().collect::<Vec<_>>();
    let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
    let mut current = vec![0; right_chars.len() + 1];

    for (left_index, left_char) in left.chars().enumerate() {
        current[0] = left_index + 1;
        for (right_index, right_char) in right_chars.iter().enumerate() {
            let substitution = usize::from(left_char != *right_char);
            current[right_index + 1] = (previous[right_index + 1] + 1)
                .min(current[right_index] + 1)
                .min(previous[right_index] + substitution);
        }
        std::mem::swap(&mut previous, &mut current);
    }

    previous[right_chars.len()]
}

pub(super) const ROOT_FIELDS: &[&str] = &[
    "schema",
    "imports",
    "colors",
    "geometries",
    "morphs",
    "skins",
    "materials",
    "nodes",
    "instance_sets",
    "particles",
    "fonts",
    "labels",
    "clipping_planes",
    "animations",
    "cameras",
    "lights",
    "scene",
    "render",
    "expect",
    "section_box",
    "measurements",
    "callouts",
    "exploded_view",
    "capture",
    "metadata",
];
pub(super) const IMPORT_FIELDS: &[&str] =
    &["id", "uri", "optional", "transform", "expected_extent"];
pub(super) const CAPTURE_FIELDS: &[&str] = &["width", "height"];
pub(super) const EXPECTED_EXTENT_FIELDS: &[&str] = &["min", "max", "unit"];
pub(super) const UNSUPPORTED_WORKFLOW_FIELDS: &[&str] = &[
    "steps",
    "sequence",
    "sequences",
    "loop",
    "loops",
    "branch",
    "branches",
    "timeline",
    "script",
];
pub(super) const UNSUPPORTED_SECTION_FIELDS: &[&str] = &[
    "primitives",
    "viewer_profile",
    "environment",
    "placements",
    "anchors",
    "connectors",
    "bounds",
    "named_states",
];