1-
21/**
32 * Simple deep equality check for two objects
43 * @param {Record<string, unknown> } a - The first object
@@ -16,23 +15,61 @@ export function deepEqual(a: Record<string, unknown>, b: Record<string, unknown>
1615 return aKeys . every ( key => deepEqual ( a [ key ] as Record < string , unknown > , b [ key ] as Record < string , unknown > ) ) ;
1716}
1817
19- type Equalable = {
20- equals : ( other : unknown ) => boolean ;
21- } ;
22-
23- function hasEqualsMethod ( obj : unknown ) : obj is Equalable {
24- return typeof obj === 'object' &&
25- obj !== null &&
26- 'equals' in obj &&
27- typeof ( obj as Equalable ) . equals === 'function' ;
28- }
29-
30- export const shallowEquals = ( objA : Record < string , unknown > , objB : Record < string , unknown > ) : boolean => {
18+ interface Approximate {
19+ equalsApprox ( other : unknown ) : boolean ;
20+ }
21+
22+ interface Equatable {
23+ equals ( other : unknown ) : boolean ;
24+ }
25+
26+ /**
27+ * Check if two values are equal. Handles primitives, null/undefined,
28+ * and objects with `equalsApprox` or `equals` methods (Vec3, Color, etc.)
29+ *
30+ * Priority order:
31+ * 1. Strict equality (===)
32+ * 2. Floating point approximation (equalsApprox) - handles precision drift
33+ * 3. Structural equality (equals)
34+ *
35+ * @param a - First value to compare
36+ * @param b - Second value to compare
37+ * @returns True if values are equal, false otherwise
38+ */
39+ export function valuesEqual ( a : unknown , b : unknown ) : boolean {
40+ if ( a === b ) return true ;
41+
42+ // Early exit if either is null/undefined (using type coercion)
43+ if ( a == null || b == null ) return false ;
44+
45+ if ( typeof a === 'object' ) {
46+ // Priority 1: Floating point approximation (handles precision drift)
47+ if ( 'equalsApprox' in a && typeof ( a as Approximate ) . equalsApprox === 'function' ) {
48+ return ( a as Approximate ) . equalsApprox ( b ) ;
49+ }
50+
51+ // Priority 2: Strict structural equality
52+ if ( 'equals' in a && typeof ( a as Equatable ) . equals === 'function' ) {
53+ return ( a as Equatable ) . equals ( b ) ;
54+ }
55+ }
56+
57+ // For other objects, return false to trigger re-apply (conservative)
58+ return false ;
59+ }
60+
61+ /**
62+ * Shallow equality check for two objects. Compares each property using valuesEqual.
63+ *
64+ * @param objA - First object to compare
65+ * @param objB - Second object to compare
66+ * @returns True if objects are shallowly equal, false otherwise
67+ */
68+ export const shallowEquals = ( objA : Record < string , unknown > , objB : Record < string , unknown > ) : boolean => {
3169 // If the two objects are the same object, return true
3270 if ( objA === objB ) {
3371 return true ;
3472 }
35-
3673
3774 // If either is not an object (null or primitives), return false
3875 if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) {
@@ -48,15 +85,10 @@ type Equalable = {
4885 return false ;
4986 }
5087
51- // Check if all keys and their values are equal
88+ // Check if all keys and their values are equal using valuesEqual
5289 for ( let i = 0 ; i < keysA . length ; i ++ ) {
5390 const key = keysA [ i ] ;
54- const propA = objA [ key ] ;
55- const propB = objB [ key ] ;
56- // If the object has an equality operator, use this
57- if ( hasEqualsMethod ( propA ) ) {
58- return propA . equals ( propB ) ;
59- } else if ( propA !== propB ) {
91+ if ( ! valuesEqual ( objA [ key ] , objB [ key ] ) ) {
6092 return false ;
6193 }
6294 }
0 commit comments