Skip to content

fix(clone): preserve target instance + Map/Set deep clone + cycle detection#2992

Open
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-manager-prefab
Open

fix(clone): preserve target instance + Map/Set deep clone + cycle detection#2992
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-manager-prefab

Conversation

@cptbtptpbcptdtptp
Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

Summary

Comprehensive improvements to CloneManager.cloneProperty to fix prefab clone leaking shared references and to broaden type support:

  1. Same-type instance preservation
    When a target field already has its own instance (e.g., Vector4 created by a component constructor) and the source has a distinct instance of the same class, the undecorated clone mode is upgraded to Deep so the target's independent instance is preserved (filled via copyFrom) instead of being overwritten with the prefab's shared reference.

  2. Map / Set deep clone support
    Native Map and Set properties are now cloned element-by-element instead of being skipped.

  3. Cycle / shared-reference detection
    The default Object branch checks deepInstanceMap first, so cyclic and shared references within a single clone graph preserve identity.

  4. CloneMode.Assignment is never upgraded
    An explicit @assignmentClone is respected even when target has a same-type instance — user-declared reference semantics win over the auto-upgrade.

  5. Restructured control flow
    The dispatch is now organized into explicit numbered phases (remap → ignore → primitive → effective mode → type-specific clone) for clarity.

Also: ModelMesh throw stringthrow Error for usable stack traces.

Origin

Squashed from commits on fix/shaderlab branch:

  • a6156ba9f (luzhuang) — main fix
  • ca5252a9a — cycle detection
  • 5c2edee1e / 40006d952 / f5be42400 — restructure + Assignment carve-out

Submitted as one PR because the original chain had misleading commit titles ("raycast and clone" / "feat: supported filled" etc.) — net result is what matters for review.

Test Plan

  • Existing CloneUtils.test.ts and ShaderData.test.ts pass against the new implementation
  • Prefab with Vector4 member field: clone does not share reference with template
  • Map / Set member fields: clone is deeply isolated
  • Cyclic reference graph: clone preserves the cycle (does not infinite-loop or duplicate)
  • @assignmentClone field with same-type target instance: still gets reference assignment (not upgraded)

Summary by CodeRabbit

  • Bug Fixes

    • Improved error reporting for unreadable vertex buffers.
  • Refactor

    • Cloning/duplication now more consistently deep-copies undecorated plain objects, arrays, Maps and Sets; internal entity references are remapped to cloned hierarchies while external references remain unchanged, reducing surprises when duplicating scenes or assets.

Review Change Stack

Review Change Stack

…ep clone

Multiple improvements to CloneManager:

1. **Same-type instance preservation**: When source/target properties are
   distinct instances of the same class (e.g., Vector4 fields), upgrade
   undecorated clone mode to Deep instead of overwriting with the source
   reference. Prevents prefab templates from leaking shared references
   into clone instances.

2. **Map/Set deep clone support**: Native Map and Set instances are now
   deep-cloned element-by-element instead of falling back to reference
   copy.

3. **Cycle detection**: Default Object branch now checks deepInstanceMap
   before creating a new instance, preserving identity for cyclic /
   shared references within a clone graph.

4. **Restructured control flow** with explicit numbered phases (remap →
   ignore → primitive → effective mode → type-specific clone) for clarity.

5. **Assignment mode is never upgraded** — explicit @assignmentClone is
   respected even when target has a same-type instance.

Also: ModelMesh throw string → throw Error for proper stack traces.

Originated from commits on fix/shaderlab branch:
  a6156ba (luzhuang) — main fix
  ca5252a — cycle detection
  5c2edee / 40006d9 / f5be424 — restructure + Assignment carve-out

Squashed into one PR for clean review against current dev/2.0.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Walkthrough

CloneManager restructures cloneProperty to remap references first, short-circuit Ignore, infer or upgrade clone modes for undecorated properties, and handle Map/Set/Array/Object cloning with updated target reuse and cycle registration. ModelMesh now throws Error objects for unreadable vertex buffers.

Changes

CloneManager Property Cloning Refactor

Layer / File(s) Summary
Clone Mode Inference Helper
packages/core/src/clone/CloneManager.ts
New _inferCloneMode method determines clone behavior for undecorated properties: deep-copies arrays, typed-array views, maps, sets, plain objects, and value types; reference-shares other class instances.
Cloning Decision Flow and Remapping
packages/core/src/clone/CloneManager.ts
cloneProperty restructured to prioritize _remap remapping, short-circuit Ignore, directly assign primitives/null/undefined, compute an effective clone mode (including inference and Shallow/Deep-to-Deep upgrade when target holds a same-constructor instance), and execute by effective mode.
Map and Set Cloning
packages/core/src/clone/CloneManager.ts
Dedicated Map and Set handling: initializes/clears target containers and clones/maps elements while remapping any _remap keys/values/elements.
Array Element Cloning
packages/core/src/clone/CloneManager.ts
Array cloning forwards original cloneMode into recursive cloneProperty calls, propagating decorated/undecorated inference semantics to children.
Object and Class Default Cloning
packages/core/src/clone/CloneManager.ts
Default object/class path detects cycles, reuses existing target[k] instances (or creates new ones), registers in deepInstanceMap, and clones via copyFrom or field-by-field iteration with optional _cloneTo.
Tests / Fixtures
tests/src/core/CloneUtils.test.ts
Updated WebGLEngine import; adjusted expectation for undecorated plain-object deep-copy; new fixtures and tests for undecorated entity refs in arrays, nested objects, nested arrays-of-arrays, and Map values validating cloning and internal remapping.

ModelMesh Error Handling

Layer / File(s) Summary
Vertex Buffer Error Handling
packages/core/src/mesh/ModelMesh.ts
Vertex buffer not-readable error changed from throwing a string to new Error(...) with the same message.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through clone rules, tidy and spry,
Remapped and inferred beneath a bright sky,
Maps, Sets, arrays—all copied with care,
Errors now raised as objects in air,
A rabbit applauds the clone dance, light and fair.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: target instance preservation, Map/Set deep cloning, and cycle detection, which align with the PR's core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Adds test coverage for the new CloneManager behavior:
- Undecorated `Entity[]` field — entities remapped via root
- Undecorated nested object containing entity refs
- Undecorated nested array-of-arrays of entities
- Map/Set deep clone

Authored alongside the CloneManager fix on fix/shaderlab branch.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with 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.

Inline comments:
In `@packages/core/src/clone/CloneManager.ts`:
- Around line 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).
- Around line 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.
- Around line 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.
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 46eb3fcd-2d3a-4540-8783-e5109c986971

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc2b10 and 7889f58.

📒 Files selected for processing (2)
  • packages/core/src/clone/CloneManager.ts
  • packages/core/src/mesh/ModelMesh.ts

Comment on lines +165 to +180
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);
});
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.

Comment on lines +165 to +166
case Map:
let targetPropertyM = <Map<any, any>>target[k];
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.

Comment on lines +166 to +170
let targetPropertyM = <Map<any, any>>target[k];
if (targetPropertyM == null) {
target[k] = targetPropertyM = new Map<any, any>();
} else {
targetPropertyM.clear();
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).

Comment on lines +269 to +270
// TypedArrays - copy data
if (ArrayBuffer.isView(sourceProperty)) return CloneMode.Deep;
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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/src/core/CloneUtils.test.ts (1)

1015-1035: ⚡ Quick win

Add Set clone/remap coverage to match the PR scope.

Line 1015 introduces Map coverage, but Set is still untested in this suite even though this PR claims Map/Set deep clone support. Adding one Set case (internal + external entity refs) will close that regression gap.

Proposed test addition
+/** Script with UNDECORATED Set containing entity values */
+class SetRefScript extends Script {
+  entitySet: Set<Entity> = new Set();
+}
+
+describe("Set with entity values (type inference)", () => {
+  it("undecorated Set should create new Set and remap internal entity values", () => {
+    const rootEntity = scene.createRootEntity("root");
+    const parent = rootEntity.createChild("parent");
+    const child = parent.createChild("child");
+    const external = rootEntity.createChild("external");
+    const script = parent.addComponent(SetRefScript);
+    script.entitySet.add(child);
+    script.entitySet.add(external);
+
+    const cloned = parent.clone();
+    const cs = cloned.getComponent(SetRefScript);
+
+    expect(cs.entitySet).not.eq(script.entitySet);
+    expect(cs.entitySet.size).eq(2);
+    expect(cs.entitySet.has(cloned.children[0])).eq(true);
+    expect(cs.entitySet.has(external)).eq(true);
+
+    rootEntity.destroy();
+  });
+});
🤖 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 `@tests/src/core/CloneUtils.test.ts` around lines 1015 - 1035, Add a parallel
test for Set cloning/remapping to the Map case: create
rootEntity/parent/child/external, attach the component that holds a Set (e.g.,
SetRefScript or the component that defines entitySet), add the internal child
and external entity to script.entitySet, clone parent (parent.clone()), retrieve
the cloned component, and assert the cloned entitySet is a different Set
instance, has size 2, contains the remapped internal reference
(cloned.children[0]) and still contains the original external reference; then
destroy rootEntity. Use the same test structure/location as the existing "Map
with entity values (type inference)" test so Set coverage matches Map.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@tests/src/core/CloneUtils.test.ts`:
- Around line 1015-1035: Add a parallel test for Set cloning/remapping to the
Map case: create rootEntity/parent/child/external, attach the component that
holds a Set (e.g., SetRefScript or the component that defines entitySet), add
the internal child and external entity to script.entitySet, clone parent
(parent.clone()), retrieve the cloned component, and assert the cloned entitySet
is a different Set instance, has size 2, contains the remapped internal
reference (cloned.children[0]) and still contains the original external
reference; then destroy rootEntity. Use the same test structure/location as the
existing "Map with entity values (type inference)" test so Set coverage matches
Map.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31e2c0b6-0160-453f-a1e7-a094c123b42a

📥 Commits

Reviewing files that changed from the base of the PR and between 7889f58 and b300d2f.

📒 Files selected for processing (1)
  • tests/src/core/CloneUtils.test.ts

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 mentioned this pull request May 11, 2026
3 tasks
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

问题 状态
target 实例被 source 引用覆盖(预制件 Vector4 共享引用) ✅ 已修复(same-type instance 升级 Deep clone)
Map/Set 无深克隆支持 ✅ 已修复(element-by-element clone + Entity/Component remap)
循环引用导致无限递归 ✅ 已修复(deepInstanceMap 提前占位,在递归子字段前 set)
throw string 未包装 Error ✅ 已修复

总结

CloneManager.cloneProperty 全面升级:same-type target 实例保护、Map/Set 深克隆(含 Entity/Component remap)、循环引用检测。控制流分 6 个编号阶段,可读性显著提升。_inferCloneMode 对 undecorated 属性的类型推断覆盖了 Array/TypedArray/Map/Set/copyFrom/plain object 五类,设计合理。测试扩展完整(undecorated array/object/Map remap 全场景覆盖)。

[P2] CloneManager.ts — Map/Set 分支中普通对象 value 仅 remap,不深拷贝

Map clone 中对每个 value 执行 _remap 检查,若 value 是普通对象(Vector3{}、不实现 _remap 的自定义类)会直接把原始引用写入目标 Map,未进入 deepInstanceMap 路径,仍然共享引用。当前引擎无 Map<K, Vector3> 类用例,不阻塞合并。建议在注释中说明 Map/Set 深克隆目前只保证 Entity/Component 的 remap,普通对象 value 为浅引用,留后续迭代完整实现。

无新 P0/P1,LGTM,可合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

问题 状态
target 实例被 source 引用覆盖(预制件 Vector4 共享引用) ✅ 已修复(same-type instance 升级 Deep clone)
Map/Set 无深克隆支持 ✅ 已修复(element-by-element clone + Entity/Component remap)
循环引用导致无限递归 ✅ 已修复(deepInstanceMap 提前占位)
throw string 未包装 Error ✅ 已修复

总结

CloneManager.cloneProperty 全面升级:same-type target 实例保护、Map/Set 深克隆(含 Entity/Component remap)、循环引用检测。控制流分 6 个编号阶段,可读性显著提升。_inferCloneMode 对 undecorated 属性的类型推断覆盖了 Array/TypedArray/Map/Set/copyFrom/plain object 五类,设计合理。测试扩展完整(undecorated array/object/Map remap 全场景覆盖)。

问题

[P2] CloneManager.ts — Map/Set 分支中普通对象 value 仅 remap,不深拷贝

Map clone 中对每个 value 执行 _remap 检查,若 value 是普通对象(Vector3{}、不实现 _remap 的自定义类)会直接把原始引用写入目标 Map,仍然共享引用。当前引擎无 Map<K, Vector3> 类用例,不阻塞合并。建议在注释中说明 Map/Set 深克隆目前只保证 Entity/Component 的 remap,普通对象 value 为浅引用,留后续迭代完整实现。

无新 P0/P1,LGTM,可合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

审查(2026-05-15)

已关闭问题清单

问题 状态
target 实例被 source 引用覆盖(预制件 Vector4 共享引用) ✅ 已修复:same-type instance 升级 Deep clone
Map/Set 无深克隆支持 ✅ 已修复(element-by-element clone + Entity/Component remap)
循环引用导致无限递归 ✅ 已修复(deepInstanceMap 检测)

总结

CloneManager.cloneProperty 三项改进方向均正确:same-type instance 保留、Map/Set 深克隆、循环/共享引用检测。控制流重构成 6 个编号阶段也提升了可读性。ModelMesh throw stringthrow new Error 顺带修复。测试覆盖全面。

问题

[P2] Map clone 中对 key 做了 _remap,但 Set clone 中只对 value 做 remap

Map 支持 key 是 Entity/Component 引用(如 HashMap<Entity, data> 场景),Set 目前只处理 value。对称性上 Set 的 remap 已正确(Set 没有 key)。但 Map 的 key remap 在当前版本实际上是必要的,而 Set 因为 value 本身就是 key 所以已对称——P3 级,不影响合入。

整体 LGTM,可以合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

审查(2026-05-15)

已关闭问题清单

问题 状态
target 实例被 source 引用覆盖(预制件 Vector4 共享引用) ✅ 已修复:same-type instance 升级 Deep clone
Map/Set 无深克隆支持 ✅ 已修复(element-by-element clone + Entity/Component remap)
循环引用导致无限递归 ✅ 已修复(deepInstanceMap 在递归前 set,阻断环)
Array 子元素克隆:传 original cloneMode 而非 effectiveCloneMode ✅ 注释说明了理由(decorated 子元素继承 decorator 声明的模式,undecorated 子元素独立推断)

总结

CloneManager 三项扩展全部正确且实现质量高:

  • same-type instance 保留@assignmentClone 永不升级,其他 mode + same-type target 时升级为 Deep,精确保护了组件构造函数创建的独立实例。
  • Map/Set 深克隆:element-by-element 处理 + Entity/Component remap,与 Array 路径对称。
  • 循环检测deepInstanceMap.set 在递归前调用(Object default 分支),时序正确。
  • _inferCloneMode:将推断逻辑从主路径中提取为私有 helper,控制流清晰。

ModelMesh throw stringthrow new Error 是顺带的好修复。

问题

[P2] CloneManager.ts:155-175(Map 分支)— Map key 的 remap 语义存疑

Map key 被 remap 了(Entity/Component 类型的 key 替换为克隆后的实例),但 Set 的 value 也被 remap 了。这两种行为是正确的,但文档缺失:当 Map 的 key 是场景内 Entity 时,克隆出来的新 Map 的 key 会指向克隆层级内的对应 Entity(remap)还是原始 Entity(reference)?当前实现是 remap,但如果 key 来自场景外部 Entity(无法 remap),_remap 返回的是什么?建议在 Map/Set 分支旁加注释说明 remap 行为,和"external ref 保持不变"的保证。

这是文档缺失问题,不阻塞合入。

可合入

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

总结

CloneManager.cloneProperty 三项改进全部正确且实现质量高:

  • same-type instance 保留@assignmentClone 永不升级,其他 mode + same-type target 时升级为 Deep,精确保护了组件构造函数创建的独立实例(修复预制件 Vector4 共享引用 bug)。
  • Map/Set 深克隆:element-by-element 处理 + Entity/Component remap,覆盖 Map key 也能 remap 的场景。
  • 循环引用检测deepInstanceMap.set 在递归子字段之前完成,正确阻断环。

控制流重构成 6 个编号阶段,可读性显著提升。ModelMesh throw stringthrow new Error 顺带修复。测试扩展全面(undecorated array/object/Map remap 全场景覆盖)。

所有历史问题均已修复。

问题

[P2] CloneManager.ts:206 — Array 子元素克隆传 cloneMode(原始 decorator mode)而非 effectiveCloneMode

注释已说明理由("decorated → children inherit, undecorated → children infer independently"),逻辑正确。但这意味着如果用 @shallowClone 装饰了一个 Array 字段,Array 内部的 Object 元素会走 Shallow 而非继续推断 Deep,这可能与用户期望不符。建议在 JSDoc 中说明 Array 元素的 clone mode 传播规则,避免使用者混淆。

[P2] _inferCloneModeArray.isArray 检查早于 copyFrom 检查,两者无重叠,顺序无影响,但如果未来有实现了 copyFrom 的 Array 子类,行为会有意外。属于防御性建议,可忽略。

无阻塞问题,LGTM,可以合并。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

问题 状态
target 实例被 source 引用覆盖(Vector4 预制件 shared reference) ✅ 已修复:same-type instance 升级 Deep clone
Map/Set 无深克隆支持 ✅ 已修复(element-by-element clone + Entity/Component remap)
循环引用导致无限递归 ✅ 已修复(deepInstanceMap.set 在递归前完成,正确阻断环)

总结

CloneManager.cloneProperty 三项改进全部正确且实现质量高:

  • same-type instance 保留@assignmentClone 永不升级,其他 mode + same-type target 时升级为 Deep,精确保护了组件构造函数创建的独立实例
  • Map/Set 深克隆:element-by-element 处理 + Entity/Component remap,Map key remap 也覆盖
  • 循环引用检测deepInstanceMap.set 在递归子字段之前完成,正确阻断环

ModelMesh throw stringthrow new Error 顺带修复。测试扩展全面(undecorated array/object/Map remap 全场景覆盖)。

问题

[P2] CloneManager.ts:206 — Array 子元素克隆传 cloneMode(原始 decorator mode)而非 effectiveCloneMode

注释已说明理由("decorated → children inherit, undecorated → children infer independently"),逻辑正确。但这意味着如果用 @shallowClone 装饰了一个 Array 字段,Array 内部的 Object 元素会走 Shallow 而非继续推断 Deep,这可能与用户期望不符。建议在 JSDoc 中说明 Array 元素的 clone mode 继承规则,便于后续维护。


无阻塞问题,可以合并。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants