mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Remove float-cmp dependency and implement custom float comparison logic
- Eliminated the `float-cmp` crate from the project, replacing its functionality with custom float comparison functions that match Lua's epsilon-based equality checks. - Updated the `variant_eq` function to utilize the new fuzzy equality methods for `Float32` and `Float64` variants, ensuring consistent behavior across Rust and Lua implementations. - Enhanced the matching algorithms in both forward sync and syncback processes to account for refined scoring of `Ref`, `Tags`, and `Attributes`, improving scoring accuracy and parity between implementations. - Introduced a new plan document detailing the changes made to the matching algorithms, including the rationale for the updates and the expected impact on performance and correctness. These changes aim to improve the reliability and maintainability of the matching algorithms while ensuring parity with Lua's behavior.
This commit is contained in:
parent
a283fd1dc9
commit
7e0538b9f5
10 changed files with 1276 additions and 87 deletions
15
src/git.rs
15
src/git.rs
|
|
@ -786,10 +786,7 @@ mod tests {
|
|||
fs::write(dir.path().join("a.luau"), "content a").unwrap();
|
||||
git_commit_all(dir.path(), "init");
|
||||
|
||||
let refs = vec![
|
||||
"HEAD:a.luau".to_string(),
|
||||
":0:a.luau".to_string(),
|
||||
];
|
||||
let refs = vec!["HEAD:a.luau".to_string(), ":0:a.luau".to_string()];
|
||||
let results = git_batch_check_hashes(dir.path(), &refs);
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results[0].is_some());
|
||||
|
|
@ -809,19 +806,13 @@ mod tests {
|
|||
fs::write(dir.path().join("b.luau"), "staged version").unwrap();
|
||||
git_stage(dir.path(), "b.luau");
|
||||
|
||||
let refs = vec![
|
||||
"HEAD:b.luau".to_string(),
|
||||
":0:b.luau".to_string(),
|
||||
];
|
||||
let refs = vec!["HEAD:b.luau".to_string(), ":0:b.luau".to_string()];
|
||||
let results = git_batch_check_hashes(dir.path(), &refs);
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results[0].is_some());
|
||||
assert!(results[1].is_some());
|
||||
assert_ne!(results[0], results[1]);
|
||||
assert_eq!(
|
||||
results[0].as_ref().unwrap(),
|
||||
&compute_blob_sha1("original")
|
||||
);
|
||||
assert_eq!(results[0].as_ref().unwrap(), &compute_blob_sha1("original"));
|
||||
assert_eq!(
|
||||
results[1].as_ref().unwrap(),
|
||||
&compute_blob_sha1("staged version")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rbx_dom_weak::types::Ref;
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
use rbx_dom_weak::Ustr;
|
||||
|
||||
use crate::variant_eq::variant_eq;
|
||||
|
|
@ -200,8 +200,9 @@ fn build_result(
|
|||
mut snap_available: Vec<Option<InstanceSnapshot>>,
|
||||
tree_children: &[Ref],
|
||||
tree_available: &[bool],
|
||||
matched_indices: Vec<(usize, usize)>,
|
||||
mut matched_indices: Vec<(usize, usize)>,
|
||||
) -> ForwardMatchResult {
|
||||
matched_indices.sort_by_key(|&(si, _)| si);
|
||||
let mut matched = Vec::with_capacity(matched_indices.len());
|
||||
for (si, ti) in matched_indices {
|
||||
if let Some(snap) = snap_available[si].take() {
|
||||
|
|
@ -389,7 +390,7 @@ fn compute_change_count(
|
|||
None => return UNMATCHED_PENALTY,
|
||||
};
|
||||
|
||||
let mut cost = count_own_diffs(snap, &inst);
|
||||
let mut cost = count_own_diffs(snap, &inst, tree);
|
||||
if cost >= best_so_far || depth >= MAX_SCORING_DEPTH {
|
||||
return cost;
|
||||
}
|
||||
|
|
@ -440,8 +441,9 @@ fn compute_change_count(
|
|||
}
|
||||
|
||||
/// Count own property diffs between a snapshot and a tree instance.
|
||||
/// Each differing property = +1. Tags and Attributes counted granularly.
|
||||
/// Children count diff = +1.
|
||||
/// Each differing property = +1. Tags and Attributes counted granularly
|
||||
/// (per-tag and per-attribute diffs). Ref properties compared by target
|
||||
/// Name+ClassName at UNMATCHED_PENALTY per mismatch. Children count diff = +1.
|
||||
///
|
||||
/// Syncback strips default-valued properties from model/meta files, so
|
||||
/// filesystem snapshots may omit properties that the tree (populated from
|
||||
|
|
@ -449,7 +451,7 @@ fn compute_change_count(
|
|||
/// match costs with phantom diffs, properties present on only one side are
|
||||
/// skipped when their value matches the class default.
|
||||
#[inline]
|
||||
fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta) -> u32 {
|
||||
fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta, tree: &RojoTree) -> u32 {
|
||||
let mut cost: u32 = 0;
|
||||
|
||||
let snap_props = &snap.properties;
|
||||
|
|
@ -459,35 +461,30 @@ fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta) -> u32 {
|
|||
.ok()
|
||||
.and_then(|db| db.classes.get(snap.class_name.as_str()));
|
||||
|
||||
// Properties present on the snapshot side
|
||||
for (key, snap_val) in snap_props.iter() {
|
||||
if let Some(inst_val) = inst_props.get(key) {
|
||||
if !variant_eq(snap_val, inst_val) {
|
||||
cost += 1;
|
||||
}
|
||||
cost += diff_variant_pair(snap_val, inst_val, tree);
|
||||
} else {
|
||||
let is_default = class_data
|
||||
.and_then(|cd| cd.default_properties.get(key.as_str()))
|
||||
.is_some_and(|default| variant_eq(snap_val, default));
|
||||
if !is_default {
|
||||
cost += 1;
|
||||
cost += count_variant_one_sided(snap_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties present only on the tree side
|
||||
for (key, inst_val) in inst_props.iter() {
|
||||
if !snap_props.contains_key(key) {
|
||||
let is_default = class_data
|
||||
.and_then(|cd| cd.default_properties.get(key.as_str()))
|
||||
.is_some_and(|default| variant_eq(inst_val, default));
|
||||
if !is_default {
|
||||
cost += 1;
|
||||
cost += count_variant_one_sided(inst_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Children count diff
|
||||
if snap.children.len() != inst.children().len() {
|
||||
cost += 1;
|
||||
}
|
||||
|
|
@ -495,6 +492,107 @@ fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta) -> u32 {
|
|||
cost
|
||||
}
|
||||
|
||||
/// Compare two Variant values with type-aware scoring:
|
||||
/// - Ref: compare target Name+ClassName, cost UNMATCHED_PENALTY if different
|
||||
/// - Tags: +1 per tag in symmetric difference
|
||||
/// - Attributes: +1 per differing or one-sided attribute
|
||||
/// - Other: +1 if not equal
|
||||
#[inline]
|
||||
fn diff_variant_pair(a: &Variant, b: &Variant, tree: &RojoTree) -> u32 {
|
||||
match (a, b) {
|
||||
(Variant::Ref(ref_a), Variant::Ref(ref_b)) => {
|
||||
if ref_a == ref_b {
|
||||
return 0;
|
||||
}
|
||||
let a_none = !ref_a.is_some();
|
||||
let b_none = !ref_b.is_some();
|
||||
if a_none != b_none {
|
||||
return UNMATCHED_PENALTY;
|
||||
}
|
||||
if a_none && b_none {
|
||||
return 0;
|
||||
}
|
||||
let target_a = tree.get_instance(*ref_a);
|
||||
let target_b = tree.get_instance(*ref_b);
|
||||
match (target_a, target_b) {
|
||||
(Some(ta), Some(tb)) => {
|
||||
if ta.name() == tb.name() && ta.class_name() == tb.class_name() {
|
||||
0
|
||||
} else {
|
||||
UNMATCHED_PENALTY
|
||||
}
|
||||
}
|
||||
(None, None) => 0,
|
||||
_ => UNMATCHED_PENALTY,
|
||||
}
|
||||
}
|
||||
(Variant::Tags(tags_a), Variant::Tags(tags_b)) => count_tags_diff(tags_a, tags_b),
|
||||
(Variant::Attributes(attrs_a), Variant::Attributes(attrs_b)) => {
|
||||
count_attributes_diff(attrs_a, attrs_b)
|
||||
}
|
||||
_ => {
|
||||
if variant_eq(a, b) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cost for a one-sided Variant (property present on only one side).
|
||||
/// Tags/Attributes contribute their element count; Refs cost UNMATCHED_PENALTY;
|
||||
/// other types cost 1.
|
||||
#[inline]
|
||||
fn count_variant_one_sided(val: &Variant) -> u32 {
|
||||
match val {
|
||||
Variant::Ref(r) => {
|
||||
if r.is_some() {
|
||||
UNMATCHED_PENALTY
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Variant::Tags(tags) => tags.len() as u32,
|
||||
Variant::Attributes(attrs) => attrs.len() as u32,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Count tag diffs as symmetric difference (matching Lua per-tag scoring).
|
||||
fn count_tags_diff(a: &rbx_dom_weak::types::Tags, b: &rbx_dom_weak::types::Tags) -> u32 {
|
||||
use std::collections::HashSet;
|
||||
let set_a: HashSet<&str> = a.iter().collect();
|
||||
let set_b: HashSet<&str> = b.iter().collect();
|
||||
let only_a = set_a.difference(&set_b).count();
|
||||
let only_b = set_b.difference(&set_a).count();
|
||||
(only_a + only_b) as u32
|
||||
}
|
||||
|
||||
/// Count attribute diffs granularly (matching Lua per-attribute scoring).
|
||||
fn count_attributes_diff(
|
||||
a: &rbx_dom_weak::types::Attributes,
|
||||
b: &rbx_dom_weak::types::Attributes,
|
||||
) -> u32 {
|
||||
let mut cost: u32 = 0;
|
||||
for (key, a_val) in a.iter() {
|
||||
match b.get(key.as_str()) {
|
||||
Some(b_val) => {
|
||||
if !variant_eq(a_val, b_val) {
|
||||
cost += 1;
|
||||
}
|
||||
}
|
||||
None => cost += 1,
|
||||
}
|
||||
}
|
||||
for (key, _) in b.iter() {
|
||||
if a.get(key.as_str()).is_none() {
|
||||
cost += 1;
|
||||
}
|
||||
}
|
||||
cost
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1295,4 +1393,207 @@ mod tests {
|
|||
assert_eq!(r1.matched.len(), r2.matched.len());
|
||||
assert_eq!(r1.unmatched_snapshot.len(), r2.unmatched_snapshot.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tags_granular_scoring() {
|
||||
use rbx_dom_weak::types::{Tags, Variant};
|
||||
|
||||
fn make_tags(names: &[&str]) -> Tags {
|
||||
let mut t = Tags::new();
|
||||
for &n in names {
|
||||
t.push(n);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
let snap_a = make_snapshot_with_props(
|
||||
"Item",
|
||||
"Part",
|
||||
vec![("Tags", Variant::Tags(make_tags(&["A", "B", "C"])))],
|
||||
);
|
||||
let snap_b = make_snapshot_with_props(
|
||||
"Item",
|
||||
"Part",
|
||||
vec![("Tags", Variant::Tags(make_tags(&["X", "Y"])))],
|
||||
);
|
||||
|
||||
let tree_a = make_snapshot_with_props(
|
||||
"Item",
|
||||
"Part",
|
||||
vec![("Tags", Variant::Tags(make_tags(&["A", "B", "C"])))],
|
||||
);
|
||||
let tree_b = make_snapshot_with_props(
|
||||
"Item",
|
||||
"Part",
|
||||
vec![("Tags", Variant::Tags(make_tags(&["X", "Y"])))],
|
||||
);
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(vec![tree_b, tree_a]);
|
||||
let result = match_forward(
|
||||
vec![snap_a, snap_b],
|
||||
&tree_refs,
|
||||
&tree,
|
||||
&MatchingSession::new(),
|
||||
);
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
for (snap, tree_ref) in &result.matched {
|
||||
let inst = tree.get_instance(*tree_ref).unwrap();
|
||||
let snap_tags = snap.properties.get(&ustr("Tags"));
|
||||
let tree_tags = inst.properties().get(&ustr("Tags"));
|
||||
assert!(
|
||||
variant_eq(snap_tags.unwrap(), tree_tags.unwrap()),
|
||||
"Tags mismatch: snap={:?}, tree={:?}",
|
||||
snap_tags,
|
||||
tree_tags
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_granular_scoring() {
|
||||
use rbx_dom_weak::types::{Attributes, Variant};
|
||||
|
||||
let mut attrs_a = Attributes::new();
|
||||
attrs_a.insert("Health".into(), Variant::Float32(100.0));
|
||||
attrs_a.insert("Speed".into(), Variant::Float32(16.0));
|
||||
|
||||
let mut attrs_b = Attributes::new();
|
||||
attrs_b.insert("Health".into(), Variant::Float32(50.0));
|
||||
attrs_b.insert("Damage".into(), Variant::Float32(25.0));
|
||||
|
||||
let snap_a = make_snapshot_with_props(
|
||||
"Mob",
|
||||
"Part",
|
||||
vec![("Attributes", Variant::Attributes(attrs_a.clone()))],
|
||||
);
|
||||
let snap_b = make_snapshot_with_props(
|
||||
"Mob",
|
||||
"Part",
|
||||
vec![("Attributes", Variant::Attributes(attrs_b.clone()))],
|
||||
);
|
||||
|
||||
let tree_a = make_snapshot_with_props(
|
||||
"Mob",
|
||||
"Part",
|
||||
vec![("Attributes", Variant::Attributes(attrs_a))],
|
||||
);
|
||||
let tree_b = make_snapshot_with_props(
|
||||
"Mob",
|
||||
"Part",
|
||||
vec![("Attributes", Variant::Attributes(attrs_b))],
|
||||
);
|
||||
|
||||
let (tree, tree_refs) = make_tree_from_snapshots(vec![tree_b, tree_a]);
|
||||
let result = match_forward(
|
||||
vec![snap_a, snap_b],
|
||||
&tree_refs,
|
||||
&tree,
|
||||
&MatchingSession::new(),
|
||||
);
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
for (snap, tree_ref) in &result.matched {
|
||||
let inst = tree.get_instance(*tree_ref).unwrap();
|
||||
let snap_attrs = snap.properties.get(&ustr("Attributes"));
|
||||
let tree_attrs = inst.properties().get(&ustr("Attributes"));
|
||||
assert!(
|
||||
variant_eq(snap_attrs.unwrap(), tree_attrs.unwrap()),
|
||||
"Attributes mismatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_scoring_forward_sync() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
// Build a tree with 2 Models ("Weapon"), each containing a differently
|
||||
// named child Part. A Ref property on each Model points to its child.
|
||||
// The matching must pair each snapshot Model to the tree Model whose
|
||||
// Ref target has the same Name+ClassName.
|
||||
|
||||
// Build a tree with 2 Models ("Weapon"), each containing a
|
||||
// differently-named child Part. Ref property on each Model points
|
||||
// to its child. Tree order is reversed relative to snapshots.
|
||||
let tree_child_handle = {
|
||||
let mut s = make_snapshot("Handle", "Part");
|
||||
s.properties
|
||||
.insert(ustr("Transparency"), Variant::Float32(0.0));
|
||||
s
|
||||
};
|
||||
let tree_child_grip = {
|
||||
let mut s = make_snapshot("Grip", "Part");
|
||||
s.properties
|
||||
.insert(ustr("Transparency"), Variant::Float32(0.0));
|
||||
s
|
||||
};
|
||||
let tree_model_grip_snap = make_snapshot_with_children(
|
||||
"Weapon",
|
||||
"Model",
|
||||
vec![tree_child_grip],
|
||||
);
|
||||
let tree_model_handle_snap = make_snapshot_with_children(
|
||||
"Weapon",
|
||||
"Model",
|
||||
vec![tree_child_handle],
|
||||
);
|
||||
|
||||
let (tree2, tree_refs2) =
|
||||
make_tree_from_snapshots(vec![tree_model_grip_snap, tree_model_handle_snap]);
|
||||
|
||||
let tree2_model_0 = tree2.get_instance(tree_refs2[0]).unwrap();
|
||||
let tree2_model_1 = tree2.get_instance(tree_refs2[1]).unwrap();
|
||||
let tree2_grip_ref = tree2_model_0.children()[0];
|
||||
let tree2_handle_ref = tree2_model_1.children()[0];
|
||||
|
||||
// Rebuild snapshots with Refs pointing into tree2
|
||||
let snap_handle2 = {
|
||||
let mut s = make_snapshot_with_children(
|
||||
"Weapon",
|
||||
"Model",
|
||||
vec![make_snapshot("Handle", "Part")],
|
||||
);
|
||||
s.properties
|
||||
.insert(ustr("PrimaryPart"), Variant::Ref(tree2_handle_ref));
|
||||
s
|
||||
};
|
||||
let snap_grip2 = {
|
||||
let mut s = make_snapshot_with_children(
|
||||
"Weapon",
|
||||
"Model",
|
||||
vec![make_snapshot("Grip", "Part")],
|
||||
);
|
||||
s.properties
|
||||
.insert(ustr("PrimaryPart"), Variant::Ref(tree2_grip_ref));
|
||||
s
|
||||
};
|
||||
|
||||
let result = match_forward(
|
||||
vec![snap_handle2, snap_grip2],
|
||||
&tree_refs2,
|
||||
&tree2,
|
||||
&MatchingSession::new(),
|
||||
);
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_snapshot.is_empty());
|
||||
assert!(result.unmatched_tree.is_empty());
|
||||
|
||||
// Verify correct pairing: each snap should match the tree model
|
||||
// whose child has the same name as the snap's child.
|
||||
for (snap, tree_ref) in &result.matched {
|
||||
let snap_child_name = &snap.children[0].name;
|
||||
let tree_inst = tree2.get_instance(*tree_ref).unwrap();
|
||||
let tree_child_ref = tree_inst.children()[0];
|
||||
let tree_child = tree2.get_instance(tree_child_ref).unwrap();
|
||||
assert_eq!(
|
||||
snap_child_name.as_ref(),
|
||||
tree_child.name(),
|
||||
"Ref scoring failed: snap with child '{}' matched tree with child '{}'",
|
||||
snap_child_name,
|
||||
tree_child.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use std::cell::RefCell;
|
|||
use std::collections::HashMap;
|
||||
|
||||
use blake3::Hash;
|
||||
use rbx_dom_weak::types::Ref;
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
use rbx_dom_weak::{Ustr, WeakDom};
|
||||
|
||||
use crate::variant_eq::variant_eq;
|
||||
|
|
@ -214,7 +214,7 @@ fn build_result(
|
|||
old_matched: &[bool],
|
||||
mut matched_indices: Vec<(usize, usize)>,
|
||||
) -> MatchResult {
|
||||
matched_indices.sort_by_key(|&(ni, _)| ni); // TODO: this seems sketchy?
|
||||
matched_indices.sort_by_key(|&(ni, _)| ni);
|
||||
let matched: Vec<(Ref, Ref)> = matched_indices
|
||||
.into_iter()
|
||||
.map(|(ni, oi)| (new_children[ni], old_children[oi]))
|
||||
|
|
@ -492,7 +492,9 @@ fn compute_change_count(
|
|||
}
|
||||
|
||||
/// Count own property diffs between two WeakDom instances (flat, non-recursive).
|
||||
/// Each differing property = +1. Children count diff = +1.
|
||||
/// Tags and Attributes counted granularly (per-tag and per-attribute diffs).
|
||||
/// Ref properties compared by target Name+ClassName at UNMATCHED_PENALTY per
|
||||
/// mismatch. Children count diff = +1.
|
||||
/// Does NOT include hash fast-path (handled by `compute_change_count`).
|
||||
///
|
||||
/// Properties present on only one side are skipped when their value matches
|
||||
|
|
@ -516,15 +518,13 @@ fn count_own_diffs(new_ref: Ref, old_ref: Ref, new_dom: &WeakDom, old_dom: &Weak
|
|||
|
||||
for (key, new_val) in new_inst.properties.iter() {
|
||||
if let Some(old_val) = old_inst.properties.get(key) {
|
||||
if !variant_eq(new_val, old_val) {
|
||||
cost += 1;
|
||||
}
|
||||
cost += diff_variant_pair(new_val, old_val, new_dom, old_dom);
|
||||
} else {
|
||||
let is_default = class_data
|
||||
.and_then(|cd| cd.default_properties.get(key.as_str()))
|
||||
.is_some_and(|default| variant_eq(new_val, default));
|
||||
if !is_default {
|
||||
cost += 1;
|
||||
cost += count_variant_one_sided(new_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -535,7 +535,7 @@ fn count_own_diffs(new_ref: Ref, old_ref: Ref, new_dom: &WeakDom, old_dom: &Weak
|
|||
.and_then(|cd| cd.default_properties.get(key.as_str()))
|
||||
.is_some_and(|default| variant_eq(old_val, default));
|
||||
if !is_default {
|
||||
cost += 1;
|
||||
cost += count_variant_one_sided(old_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -547,6 +547,89 @@ fn count_own_diffs(new_ref: Ref, old_ref: Ref, new_dom: &WeakDom, old_dom: &Weak
|
|||
cost
|
||||
}
|
||||
|
||||
/// Compare two Variant values with type-aware scoring for syncback.
|
||||
/// Ref targets resolved by Name+ClassName from their respective DOMs.
|
||||
#[inline]
|
||||
fn diff_variant_pair(a: &Variant, b: &Variant, a_dom: &WeakDom, b_dom: &WeakDom) -> u32 {
|
||||
match (a, b) {
|
||||
(Variant::Ref(ref_a), Variant::Ref(ref_b)) => {
|
||||
if ref_a == ref_b {
|
||||
return 0;
|
||||
}
|
||||
let a_none = !ref_a.is_some();
|
||||
let b_none = !ref_b.is_some();
|
||||
if a_none != b_none {
|
||||
return UNMATCHED_PENALTY;
|
||||
}
|
||||
if a_none && b_none {
|
||||
return 0;
|
||||
}
|
||||
let target_a = a_dom.get_by_ref(*ref_a);
|
||||
let target_b = b_dom.get_by_ref(*ref_b);
|
||||
match (target_a, target_b) {
|
||||
(Some(ta), Some(tb)) => {
|
||||
if ta.name == tb.name && ta.class == tb.class {
|
||||
0
|
||||
} else {
|
||||
UNMATCHED_PENALTY
|
||||
}
|
||||
}
|
||||
(None, None) => 0,
|
||||
_ => UNMATCHED_PENALTY,
|
||||
}
|
||||
}
|
||||
(Variant::Tags(tags_a), Variant::Tags(tags_b)) => {
|
||||
use std::collections::HashSet;
|
||||
let set_a: HashSet<&str> = tags_a.iter().collect();
|
||||
let set_b: HashSet<&str> = tags_b.iter().collect();
|
||||
(set_a.difference(&set_b).count() + set_b.difference(&set_a).count()) as u32
|
||||
}
|
||||
(Variant::Attributes(attrs_a), Variant::Attributes(attrs_b)) => {
|
||||
let mut cost: u32 = 0;
|
||||
for (key, a_val) in attrs_a.iter() {
|
||||
match attrs_b.get(key.as_str()) {
|
||||
Some(b_val) => {
|
||||
if !variant_eq(a_val, b_val) {
|
||||
cost += 1;
|
||||
}
|
||||
}
|
||||
None => cost += 1,
|
||||
}
|
||||
}
|
||||
for (key, _) in attrs_b.iter() {
|
||||
if attrs_a.get(key.as_str()).is_none() {
|
||||
cost += 1;
|
||||
}
|
||||
}
|
||||
cost
|
||||
}
|
||||
_ => {
|
||||
if variant_eq(a, b) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cost for a one-sided Variant (property present on only one side).
|
||||
#[inline]
|
||||
fn count_variant_one_sided(val: &Variant) -> u32 {
|
||||
match val {
|
||||
Variant::Ref(r) => {
|
||||
if r.is_some() {
|
||||
UNMATCHED_PENALTY
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Variant::Tags(tags) => tags.len() as u32,
|
||||
Variant::Attributes(attrs) => attrs.len() as u32,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -784,4 +867,113 @@ mod tests {
|
|||
);
|
||||
assert_eq!(r1.matched.len(), r2.matched.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_scoring_syncback() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
// 2 Models named "Weapon" in each DOM, each with a differently-named
|
||||
// child Part. A Ref property on each Model points to its own child.
|
||||
// Syncback matching must pair by Ref target Name+ClassName, not by
|
||||
// raw Ref identity (which always differs between DOMs).
|
||||
|
||||
let mut new_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let new_root = new_dom.root_ref();
|
||||
|
||||
let new_model_a = new_dom.insert(
|
||||
new_root,
|
||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
||||
);
|
||||
let new_handle = new_dom.insert(
|
||||
new_model_a,
|
||||
InstanceBuilder::new("Part").with_name("Handle"),
|
||||
);
|
||||
new_dom
|
||||
.get_by_ref_mut(new_model_a)
|
||||
.unwrap()
|
||||
.properties
|
||||
.insert("PrimaryPart".into(), Variant::Ref(new_handle));
|
||||
|
||||
let new_model_b = new_dom.insert(
|
||||
new_root,
|
||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
||||
);
|
||||
let new_grip = new_dom.insert(
|
||||
new_model_b,
|
||||
InstanceBuilder::new("Part").with_name("Grip"),
|
||||
);
|
||||
new_dom
|
||||
.get_by_ref_mut(new_model_b)
|
||||
.unwrap()
|
||||
.properties
|
||||
.insert("PrimaryPart".into(), Variant::Ref(new_grip));
|
||||
|
||||
// Old DOM: reversed order (Grip-model first, Handle-model second)
|
||||
let mut old_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let old_root = old_dom.root_ref();
|
||||
|
||||
let old_model_b = old_dom.insert(
|
||||
old_root,
|
||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
||||
);
|
||||
let old_grip = old_dom.insert(
|
||||
old_model_b,
|
||||
InstanceBuilder::new("Part").with_name("Grip"),
|
||||
);
|
||||
old_dom
|
||||
.get_by_ref_mut(old_model_b)
|
||||
.unwrap()
|
||||
.properties
|
||||
.insert("PrimaryPart".into(), Variant::Ref(old_grip));
|
||||
|
||||
let old_model_a = old_dom.insert(
|
||||
old_root,
|
||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
||||
);
|
||||
let old_handle = old_dom.insert(
|
||||
old_model_a,
|
||||
InstanceBuilder::new("Part").with_name("Handle"),
|
||||
);
|
||||
old_dom
|
||||
.get_by_ref_mut(old_model_a)
|
||||
.unwrap()
|
||||
.properties
|
||||
.insert("PrimaryPart".into(), Variant::Ref(old_handle));
|
||||
|
||||
let new_children: Vec<Ref> = new_dom.get_by_ref(new_root).unwrap().children().to_vec();
|
||||
let old_children: Vec<Ref> = old_dom.get_by_ref(old_root).unwrap().children().to_vec();
|
||||
|
||||
let result = match_children(
|
||||
&new_children,
|
||||
&old_children,
|
||||
&new_dom,
|
||||
&old_dom,
|
||||
None,
|
||||
None,
|
||||
&MatchingSession::new(),
|
||||
);
|
||||
|
||||
assert_eq!(result.matched.len(), 2);
|
||||
assert!(result.unmatched_new.is_empty());
|
||||
assert!(result.unmatched_old.is_empty());
|
||||
|
||||
for (new_ref, old_ref) in &result.matched {
|
||||
let new_inst = new_dom.get_by_ref(*new_ref).unwrap();
|
||||
let old_inst = old_dom.get_by_ref(*old_ref).unwrap();
|
||||
|
||||
let new_child_name = &new_dom
|
||||
.get_by_ref(new_inst.children()[0])
|
||||
.unwrap()
|
||||
.name;
|
||||
let old_child_name = &old_dom
|
||||
.get_by_ref(old_inst.children()[0])
|
||||
.unwrap()
|
||||
.name;
|
||||
assert_eq!(
|
||||
new_child_name, old_child_name,
|
||||
"Ref scoring failed: new model with child '{}' matched old model with child '{}'",
|
||||
new_child_name, old_child_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,34 @@
|
|||
use rbx_dom_weak::types::{PhysicalProperties, Variant, Vector3};
|
||||
|
||||
/// Accepts three argumets: a float type and two values to compare.
|
||||
///
|
||||
/// Returns a bool indicating whether they're equal. This accounts for NaN such
|
||||
/// that `approx_eq!(f32, f32::NAN, f32::NAN)` is `true`.
|
||||
macro_rules! approx_eq {
|
||||
($Ty:ty, $a:expr, $b:expr) => {
|
||||
float_cmp::approx_eq!($Ty, $a, $b) || $a.is_nan() && $b.is_nan()
|
||||
};
|
||||
const EPSILON_F32: f32 = 0.0001;
|
||||
const EPSILON_F64: f64 = 0.0001;
|
||||
|
||||
/// Fuzzy float equality matching Lua trueEquals: absolute OR relative epsilon.
|
||||
/// NaN == NaN is true.
|
||||
#[inline(always)]
|
||||
fn fuzzy_eq_f32(a: f32, b: f32) -> bool {
|
||||
if a.is_nan() {
|
||||
return b.is_nan();
|
||||
}
|
||||
if b.is_nan() {
|
||||
return false;
|
||||
}
|
||||
let diff = (a - b).abs();
|
||||
let max_val = a.abs().max(b.abs()).max(1.0);
|
||||
diff < EPSILON_F32 || diff < max_val * EPSILON_F32
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fuzzy_eq_f64(a: f64, b: f64) -> bool {
|
||||
if a.is_nan() {
|
||||
return b.is_nan();
|
||||
}
|
||||
if b.is_nan() {
|
||||
return false;
|
||||
}
|
||||
let diff = (a - b).abs();
|
||||
let max_val = a.abs().max(b.abs()).max(1.0);
|
||||
diff < EPSILON_F64 || diff < max_val * EPSILON_F64
|
||||
}
|
||||
|
||||
/// Compares two variants to determine if they're equal. This correctly takes
|
||||
|
|
@ -46,7 +67,7 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
&& vector_eq(&a.orientation.z, &b.orientation.z)
|
||||
}
|
||||
(Variant::Color3(a), Variant::Color3(b)) => {
|
||||
approx_eq!(f32, a.r, b.r) && approx_eq!(f32, a.g, b.g) && approx_eq!(f32, a.b, b.b)
|
||||
fuzzy_eq_f32(a.r, b.r) && fuzzy_eq_f32(a.g, b.g) && fuzzy_eq_f32(a.b, b.b)
|
||||
}
|
||||
(Variant::Color3uint8(a), Variant::Color3uint8(b)) => a == b,
|
||||
(Variant::ColorSequence(a), Variant::ColorSequence(b)) => {
|
||||
|
|
@ -59,10 +80,10 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap());
|
||||
|
||||
for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) {
|
||||
if !(approx_eq!(f32, a_kp.time, b_kp.time)
|
||||
&& approx_eq!(f32, a_kp.color.r, b_kp.color.r)
|
||||
&& approx_eq!(f32, a_kp.color.g, b_kp.color.g)
|
||||
&& approx_eq!(f32, a_kp.color.b, b_kp.color.b))
|
||||
if !(fuzzy_eq_f32(a_kp.time, b_kp.time)
|
||||
&& fuzzy_eq_f32(a_kp.color.r, b_kp.color.r)
|
||||
&& fuzzy_eq_f32(a_kp.color.g, b_kp.color.g)
|
||||
&& fuzzy_eq_f32(a_kp.color.b, b_kp.color.b))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -74,8 +95,8 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
(Variant::Enum(a), Variant::Enum(b)) => a == b,
|
||||
(Variant::EnumItem(a), Variant::EnumItem(b)) => a == b,
|
||||
(Variant::Faces(a), Variant::Faces(b)) => a == b,
|
||||
(Variant::Float32(a), Variant::Float32(b)) => approx_eq!(f32, *a, *b),
|
||||
(Variant::Float64(a), Variant::Float64(b)) => approx_eq!(f64, *a, *b),
|
||||
(Variant::Float32(a), Variant::Float32(b)) => fuzzy_eq_f32(*a, *b),
|
||||
(Variant::Float64(a), Variant::Float64(b)) => fuzzy_eq_f64(*a, *b),
|
||||
(Variant::Font(a), Variant::Font(b)) => {
|
||||
a.weight == b.weight
|
||||
&& a.style == b.style
|
||||
|
|
@ -87,7 +108,7 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
(Variant::MaterialColors(a), Variant::MaterialColors(b)) => a.encode() == b.encode(),
|
||||
(Variant::NetAssetRef(a), Variant::NetAssetRef(b)) => a == b,
|
||||
(Variant::NumberRange(a), Variant::NumberRange(b)) => {
|
||||
approx_eq!(f32, a.max, b.max) && approx_eq!(f32, a.min, b.min)
|
||||
fuzzy_eq_f32(a.max, b.max) && fuzzy_eq_f32(a.min, b.min)
|
||||
}
|
||||
(Variant::NumberSequence(a), Variant::NumberSequence(b)) => {
|
||||
if a.keypoints.len() != b.keypoints.len() {
|
||||
|
|
@ -99,9 +120,9 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap());
|
||||
|
||||
for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) {
|
||||
if !(approx_eq!(f32, a_kp.time, b_kp.time)
|
||||
&& approx_eq!(f32, a_kp.value, b_kp.value)
|
||||
&& approx_eq!(f32, a_kp.envelope, b_kp.envelope))
|
||||
if !(fuzzy_eq_f32(a_kp.time, b_kp.time)
|
||||
&& fuzzy_eq_f32(a_kp.value, b_kp.value)
|
||||
&& fuzzy_eq_f32(a_kp.envelope, b_kp.envelope))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -121,12 +142,12 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
(Variant::PhysicalProperties(a), Variant::PhysicalProperties(b)) => match (a, b) {
|
||||
(PhysicalProperties::Default, PhysicalProperties::Default) => true,
|
||||
(PhysicalProperties::Custom(a2), PhysicalProperties::Custom(b2)) => {
|
||||
approx_eq!(f32, a2.density(), b2.density())
|
||||
&& approx_eq!(f32, a2.elasticity(), b2.elasticity())
|
||||
&& approx_eq!(f32, a2.friction(), b2.friction())
|
||||
&& approx_eq!(f32, a2.elasticity_weight(), b2.elasticity_weight())
|
||||
&& approx_eq!(f32, a2.friction_weight(), b2.friction_weight())
|
||||
&& approx_eq!(f32, a2.acoustic_absorption(), b2.acoustic_absorption())
|
||||
fuzzy_eq_f32(a2.density(), b2.density())
|
||||
&& fuzzy_eq_f32(a2.elasticity(), b2.elasticity())
|
||||
&& fuzzy_eq_f32(a2.friction(), b2.friction())
|
||||
&& fuzzy_eq_f32(a2.elasticity_weight(), b2.elasticity_weight())
|
||||
&& fuzzy_eq_f32(a2.friction_weight(), b2.friction_weight())
|
||||
&& fuzzy_eq_f32(a2.acoustic_absorption(), b2.acoustic_absorption())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
|
|
@ -134,10 +155,10 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
vector_eq(&a.direction, &b.direction) && vector_eq(&a.origin, &b.origin)
|
||||
}
|
||||
(Variant::Rect(a), Variant::Rect(b)) => {
|
||||
approx_eq!(f32, a.max.x, b.max.x)
|
||||
&& approx_eq!(f32, a.max.y, b.max.y)
|
||||
&& approx_eq!(f32, a.min.x, b.min.x)
|
||||
&& approx_eq!(f32, a.min.y, b.min.y)
|
||||
fuzzy_eq_f32(a.max.x, b.max.x)
|
||||
&& fuzzy_eq_f32(a.max.y, b.max.y)
|
||||
&& fuzzy_eq_f32(a.min.x, b.min.x)
|
||||
&& fuzzy_eq_f32(a.min.y, b.min.y)
|
||||
}
|
||||
(Variant::Ref(a), Variant::Ref(b)) => a == b,
|
||||
(Variant::Region3(a), Variant::Region3(b)) => {
|
||||
|
|
@ -163,18 +184,18 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
}
|
||||
}
|
||||
(Variant::UDim(a), Variant::UDim(b)) => {
|
||||
approx_eq!(f32, a.scale, b.scale) && a.offset == b.offset
|
||||
fuzzy_eq_f32(a.scale, b.scale) && a.offset == b.offset
|
||||
}
|
||||
(Variant::UDim2(a), Variant::UDim2(b)) => {
|
||||
approx_eq!(f32, a.x.scale, b.x.scale)
|
||||
fuzzy_eq_f32(a.x.scale, b.x.scale)
|
||||
&& a.x.offset == b.x.offset
|
||||
&& approx_eq!(f32, a.y.scale, b.y.scale)
|
||||
&& fuzzy_eq_f32(a.y.scale, b.y.scale)
|
||||
&& a.y.offset == b.y.offset
|
||||
}
|
||||
(Variant::UniqueId(a), Variant::UniqueId(b)) => a == b,
|
||||
(Variant::String(a), Variant::String(b)) => a == b,
|
||||
(Variant::Vector2(a), Variant::Vector2(b)) => {
|
||||
approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y)
|
||||
fuzzy_eq_f32(a.x, b.x) && fuzzy_eq_f32(a.y, b.y)
|
||||
}
|
||||
(Variant::Vector2int16(a), Variant::Vector2int16(b)) => a == b,
|
||||
(Variant::Vector3(a), Variant::Vector3(b)) => vector_eq(a, b),
|
||||
|
|
@ -189,5 +210,53 @@ pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool {
|
|||
|
||||
#[inline(always)]
|
||||
fn vector_eq(a: &Vector3, b: &Vector3) -> bool {
|
||||
approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y) && approx_eq!(f32, a.z, b.z)
|
||||
fuzzy_eq_f32(a.x, b.x) && fuzzy_eq_f32(a.y, b.y) && fuzzy_eq_f32(a.z, b.z)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fuzzy_eq_matches_lua_absolute_epsilon() {
|
||||
assert!(fuzzy_eq_f32(1.0, 1.0 + 0.000099));
|
||||
assert!(!fuzzy_eq_f32(1.0, 1.0 + 0.00011));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_eq_matches_lua_relative_epsilon() {
|
||||
assert!(fuzzy_eq_f32(10000.0, 10000.0 + 0.9));
|
||||
assert!(!fuzzy_eq_f32(10000.0, 10000.0 + 1.1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_eq_nan_handling() {
|
||||
assert!(fuzzy_eq_f32(f32::NAN, f32::NAN));
|
||||
assert!(!fuzzy_eq_f32(f32::NAN, 0.0));
|
||||
assert!(!fuzzy_eq_f32(0.0, f32::NAN));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_eq_zero_and_negative_zero() {
|
||||
assert!(fuzzy_eq_f32(0.0, -0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_eq_f64_basic() {
|
||||
assert!(fuzzy_eq_f64(1.0, 1.0 + 0.000099));
|
||||
assert!(!fuzzy_eq_f64(1.0, 1.0 + 0.00011));
|
||||
assert!(fuzzy_eq_f64(f64::NAN, f64::NAN));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variant_eq_float32_with_new_epsilon() {
|
||||
assert!(variant_eq(
|
||||
&Variant::Float32(1.0),
|
||||
&Variant::Float32(1.0 + 0.000099)
|
||||
));
|
||||
assert!(!variant_eq(
|
||||
&Variant::Float32(1.0),
|
||||
&Variant::Float32(1.0 + 0.00011)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue