Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 109 additions & 17 deletions packages/core/src/clone/CloneManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,46 @@ export class CloneManager {
): void {
const sourceProperty = source[k];

// Remappable references (Entity/Component) are always remapped, regardless of clone decorator
// 1. Remappable references (Entity/Component) are always remapped, highest priority
if (sourceProperty instanceof Object && (<ICustomClone>sourceProperty)._remap) {
target[k] = (<ICustomClone>sourceProperty)._remap(srcRoot, targetRoot);
return;
}

// 2. Explicit ignore
if (cloneMode === CloneMode.Ignore) return;

// Primitives, undecorated, or @assignmentClone: direct assign
if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) {
// 3. Primitives / null / undefined - direct assign
if (!(sourceProperty instanceof Object)) {
target[k] = sourceProperty;
return;
}

// @shallowClone / @deepClone: deep copy complex objects
// 4. Determine effective clone mode
let effectiveCloneMode: CloneMode = cloneMode;
if (effectiveCloneMode === undefined) {
// Undecorated: infer from runtime type
effectiveCloneMode = CloneManager._inferCloneMode(sourceProperty, target[k]);
} else if (effectiveCloneMode !== CloneMode.Assignment) {
// Decorated Shallow/Deep: upgrade to Deep if target already has independent same-type instance.
// Assignment is never upgraded — it means the user explicitly wants a reference copy.
const targetProperty = target[k];
if (
targetProperty &&
targetProperty !== sourceProperty &&
targetProperty.constructor === sourceProperty.constructor
) {
effectiveCloneMode = CloneMode.Deep;
}
}

// 5. Assignment - direct reference copy
if (effectiveCloneMode === CloneMode.Assignment) {
target[k] = sourceProperty;
return;
}

// 6. Shallow/Deep clone for complex types
const type = sourceProperty.constructor;
switch (type) {
case Uint8Array:
Expand All @@ -137,6 +162,37 @@ export class CloneManager {
targetPropertyT.set(<TypedArray>sourceProperty);
}
break;
case Map:
let targetPropertyM = <Map<any, any>>target[k];
Comment on lines +165 to +166
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant lines
cat -n packages/core/src/clone/CloneManager.ts | sed -n '160,230p'

Repository: galacean/engine

Length of output: 3134


🏁 Script executed:

# Check for Biome configuration
find . -type f -name "biome.json" -o -name ".biomerc*" -o -name "biome.jsonc" | head -20

Repository: galacean/engine

Length of output: 72


🏁 Script executed:

# Let's also check the complete file size to understand what we're dealing with
wc -l packages/core/src/clone/CloneManager.ts

Repository: galacean/engine

Length of output: 103


🏁 Script executed:

# Read the Biome configuration
cat biome.json

Repository: galacean/engine

Length of output: 1318


🏁 Script executed:

# Get a broader view of the switch statement to ensure no cases are missed
cat -n packages/core/src/clone/CloneManager.ts | sed -n '140,260p'

Repository: galacean/engine

Length of output: 5233


Wrap all case declarations with block scope.

The let bindings in multiple switch cases live in the switch's shared lexical scope, which is why Biome flags noSwitchDeclarations. Bracing each case creates isolated scopes and fixes the lint error. This applies to the TypedArray cases (lines 150–164), Map case (line 165), Set case (line 182), Array case (line 196), and default case (line 216).

Suggested fix
      case Uint8Array:
      case Uint16Array:
      case Uint32Array:
      case Int8Array:
      case Int16Array:
      case Int32Array:
      case Float32Array:
      case Float64Array: {
        let targetPropertyT = <TypedArray>target[k];
         if (targetPropertyT == null || targetPropertyT.length !== (<TypedArray>sourceProperty).length) {
           target[k] = (<TypedArray>sourceProperty).slice();
         } else {
           targetPropertyT.set(<TypedArray>sourceProperty);
         }
         break;
+      }
       case Map: {
         let targetPropertyM = <Map<any, any>>target[k];
         if (targetPropertyM == null) {
           target[k] = targetPropertyM = new Map<any, any>();
         } else {
           targetPropertyM.clear();
         }
         (<Map<any, any>>sourceProperty).forEach((value, key) => {
           if (key instanceof Object && (<ICustomClone>key)._remap) {
             key = (<ICustomClone>key)._remap(srcRoot, targetRoot);
           }
           if (value instanceof Object && (<ICustomClone>value)._remap) {
             value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
           }
           targetPropertyM.set(key, value);
         });
         break;
       }
       case Set: {
         let targetPropertyS = <Set<any>>target[k];
         if (targetPropertyS == null) {
           target[k] = targetPropertyS = new Set<any>();
         } else {
           targetPropertyS.clear();
         }
         (<Set<any>>sourceProperty).forEach((value) => {
           if (value instanceof Object && (<ICustomClone>value)._remap) {
             value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
           }
           targetPropertyS.add(value);
         });
         break;
       }
       case Array: {
         let targetPropertyA = <Array<any>>target[k];
         const length = (<Array<any>>sourceProperty).length;
         if (targetPropertyA == null) {
           target[k] = targetPropertyA = new Array<any>(length);
         } else {
           targetPropertyA.length = length;
         }
         for (let i = 0; i < length; i++) {
           CloneManager.cloneProperty(
             <Array<any>>sourceProperty,
             targetPropertyA,
             i,
             cloneMode,
             srcRoot,
             targetRoot,
             deepInstanceMap
           );
         }
         break;
       }
       default: {
         // Check if we've already visited this source object (cycle detection)
         if (deepInstanceMap.has(sourceProperty)) {
           target[k] = deepInstanceMap.get(sourceProperty);
           return;
         }

         let targetPropertyD = <Object>target[k];
         if (!targetPropertyD) {
           targetPropertyD = new sourceProperty.constructor();
           target[k] = targetPropertyD;
         }
         deepInstanceMap.set(sourceProperty, targetPropertyD);

         if ((<ICustomClone>sourceProperty).copyFrom) {
           (<ICustomClone>targetPropertyD).copyFrom(<ICustomClone>sourceProperty);
         } else {
           const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor);
           for (let k in sourceProperty) {
             CloneManager.cloneProperty(
               <Object>sourceProperty,
               targetPropertyD,
               k,
               cloneModes[k],
               srcRoot,
               targetRoot,
               deepInstanceMap
             );
           }
           (<ICustomClone>sourceProperty)._cloneTo?.(<ICustomClone>targetPropertyD, srcRoot, targetRoot);
         }
         break;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case Map:
let targetPropertyM = <Map<any, any>>target[k];
case Uint8Array:
case Uint16Array:
case Uint32Array:
case Int8Array:
case Int16Array:
case Int32Array:
case Float32Array:
case Float64Array: {
let targetPropertyT = <TypedArray>target[k];
if (targetPropertyT == null || targetPropertyT.length !== (<TypedArray>sourceProperty).length) {
target[k] = (<TypedArray>sourceProperty).slice();
} else {
targetPropertyT.set(<TypedArray>sourceProperty);
}
break;
}
case Map: {
let targetPropertyM = <Map<any, any>>target[k];
if (targetPropertyM == null) {
target[k] = targetPropertyM = new Map<any, any>();
} else {
targetPropertyM.clear();
}
(<Map<any, any>>sourceProperty).forEach((value, key) => {
if (key instanceof Object && (<ICustomClone>key)._remap) {
key = (<ICustomClone>key)._remap(srcRoot, targetRoot);
}
if (value instanceof Object && (<ICustomClone>value)._remap) {
value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
}
targetPropertyM.set(key, value);
});
break;
}
case Set: {
let targetPropertyS = <Set<any>>target[k];
if (targetPropertyS == null) {
target[k] = targetPropertyS = new Set<any>();
} else {
targetPropertyS.clear();
}
(<Set<any>>sourceProperty).forEach((value) => {
if (value instanceof Object && (<ICustomClone>value)._remap) {
value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
}
targetPropertyS.add(value);
});
break;
}
case Array: {
let targetPropertyA = <Array<any>>target[k];
const length = (<Array<any>>sourceProperty).length;
if (targetPropertyA == null) {
target[k] = targetPropertyA = new Array<any>(length);
} else {
targetPropertyA.length = length;
}
for (let i = 0; i < length; i++) {
CloneManager.cloneProperty(
<Array<any>>sourceProperty,
targetPropertyA,
i,
cloneMode,
srcRoot,
targetRoot,
deepInstanceMap
);
}
break;
}
default: {
// Check if we've already visited this source object (cycle detection)
if (deepInstanceMap.has(sourceProperty)) {
target[k] = deepInstanceMap.get(sourceProperty);
return;
}
let targetPropertyD = <Object>target[k];
if (!targetPropertyD) {
targetPropertyD = new sourceProperty.constructor();
target[k] = targetPropertyD;
}
deepInstanceMap.set(sourceProperty, targetPropertyD);
if ((<ICustomClone>sourceProperty).copyFrom) {
(<ICustomClone>targetPropertyD).copyFrom(<ICustomClone>sourceProperty);
} else {
const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor);
for (let k in sourceProperty) {
CloneManager.cloneProperty(
<Object>sourceProperty,
targetPropertyD,
k,
cloneModes[k],
srcRoot,
targetRoot,
deepInstanceMap
);
}
(<ICustomClone>sourceProperty)._cloneTo?.(<ICustomClone>targetPropertyD, srcRoot, targetRoot);
}
break;
}
🧰 Tools
🪛 Biome (2.4.14)

[error] 166-166: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/clone/CloneManager.ts` around lines 165 - 166, The switch
in CloneManager uses let bindings across cases causing shared lexical-scope
issues; wrap each case body in its own block (e.g., case Map: { ... break } ),
doing this for the TypedArray cases, the Map case where targetPropertyM is
declared, the Set case (targetPropertyS), the Array case (targetPropertyA/array
handling), and the default case so each let/const has its own scope and the
noSwitchDeclarations lint error is resolved.

if (targetPropertyM == null) {
target[k] = targetPropertyM = new Map<any, any>();
} else {
targetPropertyM.clear();
Comment on lines +166 to +170
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only preserve a preinitialized target when its type matches the source.

These paths currently reuse any truthy target[k]. If a field was preinitialized with a different runtime type, this can call clear() on a non-Map/Set or run copyFrom/field assignment against the wrong prototype. Gate reuse by instanceof Map / instanceof Set / matching constructor; otherwise allocate a fresh instance.

Suggested fix
-        let targetPropertyM = <Map<any, any>>target[k];
+        let targetPropertyM = target[k] instanceof Map ? <Map<any, any>>target[k] : null;
         if (targetPropertyM == null) {
           target[k] = targetPropertyM = new Map<any, any>();
         } else {
           targetPropertyM.clear();
         }

-        let targetPropertyS = <Set<any>>target[k];
+        let targetPropertyS = target[k] instanceof Set ? <Set<any>>target[k] : null;
         if (targetPropertyS == null) {
           target[k] = targetPropertyS = new Set<any>();
         } else {
           targetPropertyS.clear();
         }

         let targetPropertyD = <Object>target[k];
-        if (!targetPropertyD) {
+        if (!targetPropertyD || targetPropertyD.constructor !== sourceProperty.constructor) {
           targetPropertyD = new sourceProperty.constructor();
           target[k] = targetPropertyD;
         }

Also applies to: 183-187, 223-228

🧰 Tools
🪛 Biome (2.4.14)

[error] 166-166: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/clone/CloneManager.ts` around lines 166 - 170, The code
currently reuses any truthy target[k] (e.g., targetPropertyM) without verifying
its runtime type; update the CloneManager cloning paths that handle
Map/Set/array/cloneable objects so they only reuse a preinitialized target when
its constructor/type matches the source (use instanceof Map / instanceof Set /
Array.isArray or compare constructors, and for cloneable objects verify the
presence and prototype of copyFrom or matching constructor); if the existing
target[k] does not match, allocate a fresh instance (new Map(), new Set(), [] or
new source.constructor()) before clearing or copying into it; apply the same
guard and instantiation logic to the other occurrences noted (the
Map/Set/array/cloneable branches that use targetPropertyM, targetPropertyS, and
similar variables).

}
(<Map<any, any>>sourceProperty).forEach((value, key) => {
if (key instanceof Object && (<ICustomClone>key)._remap) {
key = (<ICustomClone>key)._remap(srcRoot, targetRoot);
}
if (value instanceof Object && (<ICustomClone>value)._remap) {
value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
}
targetPropertyM.set(key, value);
});
Comment on lines +165 to +180
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Map/Set “deep clone” still aliases nested entries.

These branches only remap _remap references, then write keys/values/elements straight into the new container. In CloneMode.Deep, nested arrays/objects/class instances are still shared with the source, and deepInstanceMap never sees the container or its entries, so self-referential/shared structures inside a Map/Set break. Route each key/value/element through cloneProperty and register the source container in deepInstanceMap before iterating.

Also applies to: 182-194

🧰 Tools
🪛 Biome (2.4.14)

[error] 166-166: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/clone/CloneManager.ts` around lines 165 - 180, The Map/Set
branches in CloneManager are only remapping _remap refs and then copying entries
directly, which causes shared nested objects to be aliased and bypasses
deepInstanceMap; fix by first registering the source container in
deepInstanceMap (deepInstanceMap.set(sourceProperty,
targetPropertyM/targetPropertyS)) immediately after creating/clearing the target
container, then iterate and clone each key/value/element via cloneProperty (pass
the same srcRoot/targetRoot/mode/context used elsewhere) instead of assigning
them directly so nested objects/arrays/class instances get deep-cloned and
self-references are preserved; apply the same change to the Set branch (lines
~182-194) and preserve any _remap handling by invoking cloneProperty which
should internally handle _remap.

break;
case Set:
let targetPropertyS = <Set<any>>target[k];
if (targetPropertyS == null) {
target[k] = targetPropertyS = new Set<any>();
} else {
targetPropertyS.clear();
}
(<Set<any>>sourceProperty).forEach((value) => {
if (value instanceof Object && (<ICustomClone>value)._remap) {
value = (<ICustomClone>value)._remap(srcRoot, targetRoot);
}
targetPropertyS.add(value);
});
break;
case Array:
let targetPropertyA = <Array<any>>target[k];
const length = (<Array<any>>sourceProperty).length;
Expand All @@ -150,46 +206,82 @@ export class CloneManager {
<Array<any>>sourceProperty,
targetPropertyA,
i,
cloneMode,
cloneMode, // Pass original mode: decorated → children inherit, undecorated → children infer independently
srcRoot,
targetRoot,
deepInstanceMap
);
}
break;
default:
let targetProperty = <Object>target[k];
// If the target property is undefined, create new instance and keep reference sharing like the source
if (!targetProperty) {
targetProperty = deepInstanceMap.get(sourceProperty);
if (!targetProperty) {
targetProperty = new sourceProperty.constructor();
deepInstanceMap.set(sourceProperty, targetProperty);
}
target[k] = targetProperty;
// Check if we've already visited this source object (cycle detection)
if (deepInstanceMap.has(sourceProperty)) {
target[k] = deepInstanceMap.get(sourceProperty);
return;
}

let targetPropertyD = <Object>target[k];
if (!targetPropertyD) {
targetPropertyD = new sourceProperty.constructor();
target[k] = targetPropertyD;
}
deepInstanceMap.set(sourceProperty, targetPropertyD);

if ((<ICustomClone>sourceProperty).copyFrom) {
(<ICustomClone>targetProperty).copyFrom(<ICustomClone>sourceProperty);
(<ICustomClone>targetPropertyD).copyFrom(<ICustomClone>sourceProperty);
} else {
const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor);
for (let k in sourceProperty) {
CloneManager.cloneProperty(
<Object>sourceProperty,
targetProperty,
targetPropertyD,
k,
cloneModes[k],
srcRoot,
targetRoot,
deepInstanceMap
);
}
(<ICustomClone>sourceProperty)._cloneTo?.(<ICustomClone>targetProperty, srcRoot, targetRoot);
(<ICustomClone>sourceProperty)._cloneTo?.(<ICustomClone>targetPropertyD, srcRoot, targetRoot);
}
break;
}
}

/**
* Infer the appropriate clone mode for an undecorated property based on its runtime type.
* This enables user custom scripts to get correct clone behavior without decorators.
*/
private static _inferCloneMode(sourceProperty: Object, targetProperty: any): CloneMode {
// If target already has an independent instance of the same type,
// deep clone to preserve isolation (e.g., constructor-created objects)
if (
targetProperty &&
targetProperty !== sourceProperty &&
targetProperty.constructor === sourceProperty.constructor
) {
return CloneMode.Deep;
}

// Arrays need recursive processing (may contain Entity/Component refs)
if (Array.isArray(sourceProperty)) return CloneMode.Deep;

// TypedArrays - copy data
if (ArrayBuffer.isView(sourceProperty)) return CloneMode.Deep;
Comment on lines +269 to +270
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In JavaScript, does ArrayBuffer.isView()return true forDataView, Uint8ClampedArray, BigInt64Array, and BigUint64Array?

💡 Result:

Yes. ArrayBuffer.isView(value) returns true for each of these, because it returns true when the argument has the internal slot [[ViewedArrayBuffer]], which is used by ArrayBuffer views such as typed array objects and DataView [1][2]. Specifically: - DataView: true [2] - Uint8ClampedArray: true (it’s a typed array view) [2][3] - BigInt64Array: true (it’s a typed array view) [2][3] - BigUint64Array: true (it’s a typed array view) [2][3] If you want to validate quickly in code: ArrayBuffer.isView(new DataView(new ArrayBuffer(1))) // true ArrayBuffer.isView(new Uint8ClampedArray(1)) // true ArrayBuffer.isView(new BigInt64Array(1)) // true ArrayBuffer.isView(new BigUint64Array(1)) // true

Citations:


🏁 Script executed:

find . -name "CloneManager.ts" -type f

Repository: galacean/engine

Length of output: 101


🏁 Script executed:

cat -n ./packages/core/src/clone/CloneManager.ts | head -350 | tail -100

Repository: galacean/engine

Length of output: 4429


🏁 Script executed:

cat -n ./packages/core/src/clone/CloneManager.ts | head -250 | tail -100

Repository: galacean/engine

Length of output: 4288


🏁 Script executed:

cat -n ./packages/core/src/clone/CloneManager.ts | sed -n '100,160p'

Repository: galacean/engine

Length of output: 2635


Handle all ArrayBuffer views explicitly in the switch statement, or narrow the ArrayBuffer.isView inference.

The ArrayBuffer.isView check at line 270 infers Deep clone for DataView, Uint8ClampedArray, BigInt64Array, and BigUint64Array, but the switch statement only handles Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, and Float64Array. The unhandled types fall through to the default case, which executes new sourceProperty.constructor(), creating empty instances that lose all buffer contents. Add explicit copy logic for the missing views (similar to lines 158–163), or restrict the inference to only the types the switch actually handles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/clone/CloneManager.ts` around lines 269 - 270, The
ArrayBuffer.isView check in CloneManager currently returns CloneMode.Deep for
all views but the switch in determineCloneMode only handles a subset
(Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array,
Float32Array, Float64Array), causing DataView, Uint8ClampedArray, BigInt64Array,
BigUint64Array to fall through and be recreated empty; update determineCloneMode
(or the surrounding logic in CloneManager) to either explicitly handle those
missing view types with copy logic like the existing TypedArray branches
(ensuring buffer contents are copied) or narrow the ArrayBuffer.isView inference
to only return Deep for the specific constructors the switch supports,
referencing ArrayBuffer.isView, determineCloneMode, CloneMode, and the listed
missing types so the behavior is deterministic and buffer contents are
preserved.


// Maps and Sets - create independent copies
if (sourceProperty instanceof Map || sourceProperty instanceof Set) return CloneMode.Deep;

// Value types with copyFrom (math types like Vector3, Color, etc.)
if ((<ICustomClone>sourceProperty).copyFrom) return CloneMode.Deep;

// Plain objects - deep clone (may contain Entity/Component refs)
if (sourceProperty.constructor === Object) return CloneMode.Deep;

// Other class instances (engine resources like Material, Texture) - shared reference
return CloneMode.Assignment;
}

static deepCloneObject(source: Object, target: Object, deepInstanceMap: Map<Object, Object>): void {
for (let k in source) {
CloneManager.cloneProperty(source, target, k, CloneMode.Deep, null, null, deepInstanceMap);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/mesh/ModelMesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ export class ModelMesh extends Mesh {
return null;
}
if (!buffer.readable) {
throw "Not allowed to access data while vertex buffer readable is false.";
throw new Error("Not allowed to access data while vertex buffer readable is false.");
}

const vertexCount = this.vertexCount;
Expand Down
Loading