Skip to content

refactor(clone): type-driven default clone mode for container elements#3040

Open
cptbtptpbcptdtptp wants to merge 59 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-opt-out-assignment
Open

refactor(clone): type-driven default clone mode for container elements#3040
cptbtptpbcptdtptp wants to merge 59 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-opt-out-assignment

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rework the clone system around a clear type-driven priority chain, so most objects clone correctly by their type with little to no per-field annotation.

How a field value is cloned — priority high → low

  1. Field decorator@deepClone / @assignmentClone / @ignoreClone (explicit per-field override, effective at every depth). @deepClone on an engine-bound value can't be honored: Entity/Component refs fall back to remap, registered assets to sharing — each with a warning.
  2. Container — Array / Map / Set / TypedArray / DataView / plain object (incl. null-prototype) → deep clone (each element re-enters the gate).
  3. Type default — the value type's @defaultCloneMode.
  4. FallbackAssignment (share the reference).

Primitives are copied by value. Functions keep the clone's own constructor-rebound binding when the slot already holds one, otherwise share the reference (so callbacks inside containers survive cloning).

Type defaults (what most fields rely on)

  • ReferResource (Texture / Mesh / Material / Sprite / Font / …) → Assignment — assets are shared by reference.
  • Entity / ComponentRemap — rewired to the clone within the cloned subtree via the identity map (single remap mechanism; the old path-walk CloneUtils is removed).
  • Math value types (Vector / Matrix / Quaternion / Color / Rect / Bounding* / Ray / …) → Deep — registered centrally in CloneManager (the math package can't depend on core's decorator); a completeness test guards that every math export with copyFrom is registered — it already caught Ray, which now implements clone() / copyFrom().
  • Value-semantic data (RenderState, particle modules, joint config, PostProcess, ShaderData, Signal, ColliderShape, ui Transition, …) → Deep.
  • Runtime containers (UpdateFlagManager, UpdateFlag, DisorderedArray, SafeLoopArray) → Ignore — the clone keeps its own constructor-built state; their former per-field @ignoreClone annotations are gone.

Ref-count model — slot-ownership contract

  • Every component top-level field sharing an explicitly registered ref-counted resource (the ReferResource family; duck-typed counters like Shader are excluded) owns one reference: the gate acquires it while cloning the slot (+1, and -1 when replacing an owned constructor preset). Releasing it on destroy is the owning class's responsibility — a class that doesn't release is a bug in that class.
  • The contract is uniform — user scripts included: a cloned script slot owns one reference like any component slot; releasing it on destroy is the script author's responsibility (onDestroy), the same way engine components release in their destroy paths.
  • Below the top level the gate never counts. The gate only auto-counts where the destroy contract is uniform and auditable — component slots; a nested host's release contract is unknowable to the gate (many nested hosts have no destroy at all), so nested classes pair the acquisition themselves in class-local code: Transition._cloneTo +1 ↔ Transition.destroy −1, all four state slots counted in the base class (SpriteTransition carries no ref-count code).
  • Slots rebuilt through setters stay @ignoreClone so the setter is the single +1 source (MeshRenderer.mesh, Renderer materials, MeshColliderShape.mesh).
  • Manual +1 compensations in Camera / Animator / AudioSource _cloneTo are removed — the gate acquisition replaces them.

Changes

  • Field decorators register as field-level modes (highest priority); CloneMode gains Ignore and is now exported.
  • ⚠️ @assignmentClone / @ignoreClone on Entity/Component refs are now honored literally (share / keep own) instead of being silently overridden by auto-remap.
  • −191 / +13 field decorators (the 13 additions are genuine runtime-state opt-outs); 37 classes registered via @defaultCloneMode. @shallowClone is removed (breaking) — migrate with @deepClone or @assignmentClone.
  • Entity/Component remap unified through the clone identity map (_registerCloneMap merged into tree construction — two recursive walks instead of three); CloneUtils, Entity._remap, Component._remap and the srcRoot/targetRoot threading are removed. _cloneTo hooks receive (target, cloneMap); Signal remaps listener targets/args by map lookup (Entity/Component args only, deterministically).
  • Container classification centralized in a single _isContainer point shared by the gate and the deep-clone dispatch; DataView clones as bytes; null-prototype objects are classified, rebuilt (Object.create(null)) and field-walked correctly; typed-array / DataView clones register in the identity map (aliasing topology preserved).
  • CloneManager.deepCloneObject honors field-level decorators like the rest of the system.
  • ColliderShape (deep-cloned shapes own their native handles; MeshColliderShape rebuilds via the mesh setter, attaching exactly once — attachment state is tracked on ColliderShape) and ui Transition registered @defaultCloneMode(Deep) — cloned colliders/interactives no longer share mutable shape/transition instances with the source.
  • Review hardening: the copyFrom value-type dispatch runs after the container branches and only for class instances with a callable copyFrom (a plain object carrying a copyFrom data key no longer crashes clone); SkinnedMeshRenderer drops the renderer-private joint texture entry from clones (GC-ignored GPU-texture leak); slot settlement (_transferSlotOwnership) is unconditional — an owned counted preset displaced by a deep-cloned value releases its reference too.
  • Docs (docs/en|zh/core/clone.mdx, how-to-contribute) rewritten to the type-driven semantics: shallowClone removed with a migration callout, default-resolution table, entity-remap example, ref-count contract section.

Pre-existing imbalances fixed alongside (exposed by the ref-count rework)

TextRenderer._font / ui Text._font (destroy released a count the clone never acquired) and MeshColliderShape._mesh (same, plus the cloned shape silently lacked a native shape) were unbalanced on dev/2.0 before this PR; the slot-ownership contract requires them fixed here to stay self-consistent. Destroying a ParticleRenderer also never released a MeshShape's mesh — fixed via a BaseShape._destroy hook on the generator destroy chain.

Test plan

  • Full local suite: 1565 tests / 118 files pass (core / ui / loader / math)
  • Multi-agent exhaustive review (6 dimensions, adversarially verified): confirmed regressions all fixed with regression tests; Skin / PostProcess / TrailRenderer clone paths now covered
  • refCount lifecycle suites (slot-ownership: clone +1 → setter churn → destroy release, per counted slot; ShaderData/Material cascade; instance materials)
  • New regressions: decorator-priority semantics, functions in containers, DataView byte-clone, null-prototype containers, math copyFrom completeness guard, collider-shape instance independence, ui transition independence, and refCount balance assertions (camera RT / animator controller / TextRenderer font / script-held texture honoring the slot contract / SpriteTransition full lifecycle)
  • CI green on bdcc6353 — build ×3 platforms, lint, codecov (patch 96.3%), e2e visual regressions ×4 shards

Merge 决策记录 — 7ba6d16f3(dev/2.0 同步合并)

本次同步合并在文本冲突解决之外,有意修改了三处上游代码行(可通过 git show 7ba6d16f3 的 combined diff 查看)。三处删除在类型驱动闸门下均行为等价:

  1. Animator.ts — 删除 fix(animation): harden animator state instance data #3024 新增的 fireEvents 上的 @assignmentClone 该装饰器进入本分支时处于"使用但未导入"状态(本分支已删除该文件的对应 import),不删会在 shader-compiler 预编译时抛 ReferenceError;布尔原始值在闸门下天然按值拷贝,装饰器冗余。
  2. VelocityOverLifetimeModule.ts — 删除 feat(particle): add orbital radial velocity over lifetime #3049 新增的 5 个 @deepClone(orbital 曲线 ×4 + _offset)。 ParticleCompositeCurveVector3 经类型注册即深拷,与同文件已去装饰器的 _velocityX/Y/Z 口径一致。由测试 "orbital velocity fields deep-clone through the type default" 钉住。
  3. ParticleCompositeCurve.ts — 删除上游为 _updateManager 补的 @ignoreClone UpdateFlagManager 已类型注册为 Ignore,字段装饰器冗余。

🤖 Generated with Claude Code

…ainer elements

- Add `@defaultCloneMode(mode)` class decorator for declaring how instances
  of a type should be cloned when encountered as field values
- Change container element cloning: array elements now use `undefined` as
  cloneMode so each element's type-level `_defaultCloneMode` is consulted.
  Unknown types (no _defaultCloneMode) default to Assignment (shared reference)
  instead of Deep, preventing crash on DOM/native objects.
- Add `_defaultCloneMode` to ICustomClone interface
- Mark RenderState system classes with @defaultCloneMode(Deep):
  DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The clone system now uses class-level default clone modes, clone-map-based identity tracking, and updated resource/reference-count handling across core engine types. Particle, physics, UI, and post-process components were adjusted to the new clone behavior, and the clone-related tests and docs were expanded accordingly.

Changes

Clone pipeline and core remapping

Layer / File(s) Summary
Clone infrastructure
packages/core/src/clone/CloneManager.ts, packages/core/src/clone/ComponentCloner.ts, packages/core/src/clone/enums/CloneMode.ts, packages/core/src/index.ts, packages/core/src/clone/CloneUtils.ts
CloneMode adds Remap, removes Shallow, and updates its docs. CloneManager adds defaultCloneMode, per-field mode registration, getFieldModes, _cloneValue, _deepClone, and counted-resource helpers, while CloneUtils is removed. ComponentCloner switches to cloneMap-based cloning and the revised ICustomClone contract.
Entity, component, and signal remapping
packages/core/src/Component.ts, packages/core/src/Entity.ts, packages/core/src/Signal.ts, packages/core/src/particle/modules/CustomDataModule.ts
Component and Entity move to @defaultCloneMode(CloneMode.Remap) and use a shared clone map through subtree cloning. Signal now remaps listeners and structured-binding arguments from the same identity map, and CustomDataModule threads that map through deep-cloned curves and gradients.

Clone-mode updates across engine, particle, physics, and UI classes

Layer / File(s) Summary
Core clone-mode updates
packages/core/src/shader/state/*, packages/core/src/Camera.ts, packages/core/src/Renderer.ts, packages/core/src/Transform.ts, packages/core/src/UpdateFlag*.ts, packages/core/src/VirtualCamera.ts, packages/core/src/animation/Animator.ts, packages/core/src/asset/ReferResource.ts, packages/core/src/audio/AudioSource.ts, packages/core/src/lighting/Light.ts, packages/core/src/mesh/Skin.ts, packages/core/src/mesh/SkinnedMeshRenderer.ts, packages/core/src/postProcess/*, packages/core/src/shader/ShaderData.ts, packages/core/src/trail/TrailRenderer.ts, packages/core/src/utils/*, packages/math/src/Ray.ts
Core engine types move to class-level default clone modes or remove explicit field decorators. Several classes add or remove clone-time cleanup and resource ownership behavior, and Ray now implements clone() and copyFrom().
Particle, physics, and UI clone behavior
packages/core/src/particle/*, packages/core/src/particle/modules/*, packages/core/src/particle/modules/shape/*, packages/core/src/physics/*, packages/ui/src/component/*
Particle modules and shape classes migrate to class-level defaults, runtime-only fields are ignored, and mesh/state teardown hooks are added. Physics and UI components also update clone decorators, cleanup, and reference-count handling to match the new pipeline.

Clone and ref-count test coverage

Layer / File(s) Summary
Clone, ref-count, and copy coverage
tests/src/core/*, tests/src/math/Ray.test.ts, tests/src/ui/*
New and expanded tests cover clone-map remapping, slot ownership and ref-count balancing, binary data cloning, function handling, math value-type copy/clone, and UI/physics transition behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: GuoLei1990

Poem

🐇 I hopped through clone maps, tidy and bright,
With deep-clone defaults and remaps just right.
Ref counts now dance and the tests sing along,
The bunny says: “cloning feels sturdy and strong!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main cloning refactor and the new type-driven default clone mode for containers.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 67-69: The defaultCloneMode decorator accepts any CloneMode value,
but the runtime logic at lines 141-149 only properly handles Deep and Shallow
modes, while Ignore is checked separately earlier at line 138. To fix this,
modify the defaultCloneMode function signature to restrict the mode parameter to
only accept CloneMode.Deep or CloneMode.Shallow (using a union type or
overload), preventing callers from incorrectly decorating with CloneMode.Ignore
which would not be honored at runtime.
🪄 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: 585fad6c-db8f-4c53-9512-ff3e147e9dde

📥 Commits

Reviewing files that changed from the base of the PR and between 5d74c1d and 79e9105.

📒 Files selected for processing (8)
  • packages/core/src/clone/CloneManager.ts
  • packages/core/src/clone/ComponentCloner.ts
  • packages/core/src/shader/state/BlendState.ts
  • packages/core/src/shader/state/DepthState.ts
  • packages/core/src/shader/state/RasterState.ts
  • packages/core/src/shader/state/RenderState.ts
  • packages/core/src/shader/state/RenderTargetBlendState.ts
  • packages/core/src/shader/state/StencilState.ts

Comment thread packages/core/src/clone/CloneManager.ts
cptbtptpbcptdtptp and others added 7 commits June 17, 2026 19:31
…+ type-driven deep clone

Core changes:
- Clone is now opt-out: all enumerable fields are cloned unless @ignoreClone
- Default clone mode for unknown types is Assignment (shared reference) — safe
  for DOM elements, native handles, and any unrecognized constructor
- Types that need independent copies declare @defaultCloneMode(CloneMode.Deep)
- Identity-map based deep clone with cycle/shared-subgraph dedup
- 3-stage lifecycle: Construct → Populate (copyFrom or for...in) → Finalize (_cloneTo)
- Container elements (Array/Map/Set) go through the type-driven gate individually
- @deepClone/@assignmentClone/@shallowClone kept as no-op for backward compat
- @ignoreClone remains functional

RenderState classes marked @defaultCloneMode(Deep):
  DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eep)

Particle system:
  ParticleGeneratorModule (+ all subclasses via inheritance),
  MainModule, BaseShape (+ all shape subclasses), ParticleGenerator,
  ParticleCompositeCurve, ParticleCurve, CurveKey,
  ParticleCompositeGradient, ParticleGradient, GradientColorKey,
  GradientAlphaKey, Burst

Post-process:
  PostProcessEffect (+ BloomEffect, TonemappingEffect via inheritance),
  PostProcessEffectParameter (+ Float/Bool/Color/Vector/Enum/Texture)

Physics:
  JointLimits, JointMotor

Other:
  VirtualCamera, Skin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mark Entity/Component with @defaultCloneMode(Remap) and split Entity.clone() into a
register-then-copy two-pass: pre-register every source Entity/Component to its clone in
the identity map across the whole subtree, so references nested in arrays/maps/objects
are remapped through the clone gate, not just top-level fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Re-register @deepClone/@assignmentClone/@ignoreClone as field-level modes (highest priority).
- Containers (Array/Map/Set/TypedArray/plain object) default to deep clone.
- Type defaults: ReferResource -> Assignment, math types -> Deep, UpdateFlagManager -> Ignore.
- Thread srcRoot/targetRoot through the gate so Signal._cloneTo can remap listeners.
- Add CloneMode.Ignore; drop erroneous @deepClone on Joint limits/motor _updateFlagManager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efaults

- Remove @deepClone/@assignmentClone/@shallowClone field decorators (171) across core & ui.
- Fields resolve via container default deep + type-level @defaultCloneMode + Assignment fallback.
- Mark ShaderData & Signal Deep; ShaderData adds _cloneTo delegating to cloneTo.
- Clean up now-unused clone decorator imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t-assignment

# Conflicts:
#	packages/core/src/particle/ParticleGenerator.ts
…l run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.82006% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.69%. Comparing base (75af66f) to head (cd3bad1).
⚠️ Report is 1 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
packages/core/src/clone/CloneManager.ts 97.73% 7 Missing ⚠️
packages/core/src/animation/AnimatorController.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3040      +/-   ##
===========================================
+ Coverage    79.52%   79.69%   +0.16%     
===========================================
  Files          904      903       -1     
  Lines       101168   101194      +26     
  Branches     11377    11457      +80     
===========================================
+ Hits         80456    80648     +192     
+ Misses       20528    20362     -166     
  Partials       184      184              
Flag Coverage Δ
unittests 79.69% <98.82%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

cptbtptpbcptdtptp and others added 6 commits June 23, 2026 15:40
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…low)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…remap via identity map

- Assignment is now a plain reference share: the gate never touches refCount.
  Ref-count ownership belongs to each class's own logic (_cloneTo hooks /
  setters), balanced by that class's destroy path. Fixes double-counting on
  Camera/Animator/AudioSource clones, Shader leak via _replacementShader, and
  permanent leaks for user-script-held resources.
- Field decorators are highest priority at all depths: the ComponentCloner
  top-level remap special case is removed; Entity/Component remap unified
  through the pre-populated cloneMap (O(1)); CloneUtils path-walk deleted.
  @deepClone on an Entity/Component ref warns and falls back to remap.
- Entity.clone() builds the tree and registers identity pairs in one walk;
  srcRoot/targetRoot threading removed — _cloneTo hooks receive cloneMap.
- Container classification centralized (single _isContainer); DataView clones
  as bytes (was: crash via generic object branch); functions in containers are
  shared instead of dropped to undefined, while constructor-rebound top-level
  handlers keep the clone's own binding.
- Register ColliderShape / ui Transition as @defaultCloneMode(Deep) so cloned
  colliders/interactives own independent instances; restore @ignoreClone +
  setter pattern for TextRenderer/Text fonts and MeshColliderShape mesh;
  SpriteTransition acquires state-sprite refs on clone.
- shallowClone decorator warns on use; dead copyProperty/copyProperties
  removed; CloneMode exported; orphan imports cleaned.
- Tests: decorator-priority semantics updated; new regressions for function
  fields, DataView, refCount balance (RT/controller/font/sprite-transition),
  collider-shape and transition instance independence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve tests/src/core/physics/ColliderShape.test.ts: physics-lite describe
removed upstream; move the shape-independence regression into the PhysX
describe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator Author

Pushed a follow-up that reworks two load-bearing decisions of this PR (design discussion happened offline):

1. The clone gate no longer touches refCount. Assignment is a plain reference share again. The gate's unconditional +1 was structurally unbalanceable: it double-counted with the manual +1s in Camera/Animator/AudioSource._cloneTo, leaked on fields no destroy path releases (user Script resources, Camera._replacementShader — Shader isn't even a ReferResource), and couldn't reach non-component hosts (SpriteTransition state sprites, Signal args). Ownership now lives entirely in each class's own logic (_cloneTo hooks / setters), balanced by that class's destroy path — the proven pre-existing pattern (MeshRenderer, Renderer, ShaderData.cloneTo). Fixed the classes that had drifted from it: TextRenderer._font / ui Text._font / MeshColliderShape runtime state are @ignoreClone + setter-in-_cloneTo; SpriteTransition._cloneTo acquires its state-sprite refs.

2. Remap is unified through the identity map and field decorators win at every depth. The ComponentCloner "_remap first" special case and the CloneUtils path-walk are gone — Entity.clone() registers every source→clone pair while building the tree (2 walks instead of 3), and the gate's Remap branch resolves in O(1) with identical out-of-subtree semantics. srcRoot/targetRoot threading is removed; _cloneTo hooks receive the cloneMap (only Signal uses it). @deepClone on an Entity/Component ref warns and falls back to remap instead of fabricating an engine-less instance. ⚠️ Semantic change: @assignmentClone/@ignoreClone on entity refs are now honored literally instead of being overridden by auto-remap (tests updated).

Also in this push: DataView clones as bytes (crashed before via the generic object branch); functions inside containers are shared instead of silently becoming undefined, while constructor-rebound top-level handlers keep the clone's own binding; container classification is a single _isContainer predicate; ColliderShape / ui Transition are registered @defaultCloneMode(Deep) so cloned colliders/buttons own independent instances (previously the clone shared and re-parented the source's shapes/transitions); dead copyProperty/copyProperties removed; shallowClone warns on use; CloneMode is exported.

Known pre-existing (not addressed): ColliderShape's constructor presets a native PhysicsMaterial that gets orphaned whenever the field is later replaced (clone or user shape.material = x) — same behavior on dev/2.0, worth its own issue.

Tests: core 907+ / physics / ui+loader all green locally; new regressions cover function fields, DataView, refCount balance (RT / controller / font / sprite-transition / script-held textures), and instance independence for collider shapes & transitions. Branch merged with latest dev/2.0 (resolved ColliderShape.test.ts against the physics-lite removal).

🤖 Generated with Claude Code

cptbtptpbcptdtptp and others added 10 commits July 3, 2026 11:09
…eleases

Component top-level fields sharing a registered ref-counted resource
(ReferResource family) acquire one reference at the clone gate (+1, and
-1 when replacing an owned preset); the owning component's destroy path
releases it (implementation contract). Scripts have no per-field destroy
logic, so gate acquisitions are recorded in a ledger and released in
Script._onDestroy. Below the top level the gate never counts: container
elements and plain-object fields are plain shares, and nested classes
pair the acquisition themselves (SpriteTransition._cloneTo +1 paired
with Transition.destroy -1, hoisted to the base class so future
ReferResource-valued transitions inherit the release).

Manual +1 compensations in Camera/Animator/AudioSource._cloneTo are
removed (the gate acquisition replaces them); TextRenderer/Text._font
return to gate accounting. The counted-resource test is explicit
registration (@defaultCloneMode(Assignment)), excluding duck-typed
counters like Shader.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leak

Add slot-ownership contract suites asserting the full lifecycle
(baseline → clone +1 → setter churn → destroy release) for every
counted component slot: Camera.renderTarget, Animator.controller,
AudioSource.clip, SpriteRenderer/SpriteMask/ui Image sprite,
MeshRenderer.mesh, MeshColliderShape.mesh, SpriteTransition states,
and the Script ledger (reassignment still releases the original).
Add ShaderData/Material cascade suites: setTexture swap/clear,
doubly-referenced hosts (±refCount propagation), renderer clone
balance, and instance-material lifecycles.

The new suites exposed a pre-existing leak: destroying a
ParticleRenderer never released the MeshShape's mesh (+1 in the mesh
setter, no release path). Add BaseShape._destroy, called from
EmissionModule._destroy, with MeshShape releasing through its setter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shallow is no longer a distinct mode; keeping the decorator silently
behaving as deep clone hides the semantic change. Migrate with
@deepClone (independent copy) or @assignmentClone (share the
reference).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Public API (getFieldModes, deepCloneObject) first, @internal entries
(_registerFieldMode, _cloneValue) after, private helpers last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Script fields are user territory: the engine cannot pair a release for
an automatic acquisition (users can't know which slots were acquired),
so the gate does no counting for script slots at all — replacing the
clone-acquisition ledger. Users who need gc protection manage counts
themselves, matching the pre-2.0 semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cloning's only ref-count job is acquiring the count for the slot it
copies. Releasing on destroy is the owning class's existing contract:
engine components in their destroy paths, script authors in onDestroy.
Remove the script exemption flag; Script.ts is untouched by this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_cloneValue is pure cloning again (4 params, Assignment = plain
share). ComponentCloner — the only place slots own references —
detects the share (result === source value, registered resource
only; remapped/deep results never match) and acquires through
CloneManager._acquireSlotOwnership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Not a priority rule: deep-copying an Entity/Component is an
unexecutable directive, so it recovers to remap with a warning.
Every executable decorator still wins over the type default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the model summary and the ref-count contract in two short
paragraphs; per-type behavior and deep-clone stages live on the
methods themselves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…shared clone

[vec3, vec3, vec3] must clone into one NEW Vector3 referenced three
times (identity-map dedup), not three copies and not the source
instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utions

CloneUtils.test.ts was the repo's last reference to the deleted class.
Three CloneTextureRefCount comments still attributed gate-acquired
references to hooks this PR removed; also cover displaced-preset release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

…llocation

The forEach arrow closures were the only unconditional intermediate
allocations on the clone path besides the clone's own output; for-of
iterators are erasable by escape analysis, closures are not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

cptbtptpbcptdtptp and others added 2 commits July 4, 2026 10:05
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

发现一个可复现的 container clone 回归,建议在合入前处理一下。

CloneManager._deepClone 在没有可复用目标对象时会走 new ctor() 创建 Deep 类型实例:

const dst = reusable ?? (ctor ? new ctor() : Object.create(null));

这对一些新注册为 @defaultCloneMode(CloneMode.Deep)、但构造函数需要参数的类型不成立。例子是 ParticleCompositeGradient:它现在是默认 Deep,但构造函数会直接访问 constantOrGradient.constructor。当它作为数组/Map/plain object 里的元素被 clone 时,容器元素没有 reuse,clone gate 会无参构造它并抛错。

我在 PR tip 上用下面这个最小用例复现了:

CloneManager._cloneValue(
  [new ParticleCompositeGradient(new Color())],
  undefined,
  new Map()
);

结果:

TypeError: Cannot read properties of undefined (reading 'constructor')

建议修复方向:要么让这些 Deep 类型支持安全的无参构造,要么给 clone 系统提供工厂/自定义构造路径;同时补一个“不带 reuse 的容器元素 clone”回归测试,覆盖数组/Map/plain object 中的 type-default Deep 值。

cptbtptpbcptdtptp and others added 2 commits July 4, 2026 20:18
The clone gate constructs container elements without arguments and then
populates every field, but the constructor dereferenced its first
parameter unconditionally — a gradient held in an array/map/plain object
crashed entity.clone(). The field defaults already form a valid
constant-mode state, so an argument-less call now simply keeps them.

Audited every Deep-registered type for the same shape: this was the only
bare-construction crash. Covers container-element cloning and the
previously untested emission-bursts engine path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed types

Cocos-style mechanism guard instead of per-class patching: the gate
creates preset-less instances with new Type(), so Deep registration
implies argument-less constructibility. A guard test enumerates every
exported Deep type across core/math/ui and bare-constructs it; the
explicit exemption list names the engine-bound structural types the
gate only ever clones against a same-type preset, with reasons.
The contract is stated on the defaultCloneMode decorator doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

…nnot construct

A user type registered Deep with a required-argument constructor used to
surface the constructor's raw TypeError from inside the gate; the failure
now states the contract (argument-less construction) and the type name,
with the original error attached as the cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

cptbtptpbcptdtptp and others added 2 commits July 4, 2026 21:37
…mantics

The exemption note overstated the preset guarantee: a user can pull a
host-bound module off a live generator and put it in a clonable
container, where the gate bare-constructs and fails. That rejection is
the intended behavior (a host-bound structure cannot exist without its
host; sharing must be declared with @assignmentClone) — state it
honestly in the exemption list and lock it with a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…order

The previous pin asserted rejection unconditionally, but when the host's
engine slot clones before the user container the identity map already
holds the module and the container reference remaps onto the clone — the
better outcome. Cover both orders: remap when the host precedes,
named-contract rejection when it does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

最后一轮看 API / 注释 / 设计自洽性,整体方向我认可:类型驱动默认行为、容器默认结构 deep、未知对象 assignment、Entity/Component 统一走 identity-map remap,ref-count 从 clone gate 移到 component slot settlement,也比递归层里做 ownership 更清楚。

还有几个建议收口:

[P2] ShaderDataTextureArray 没进入 ref-count cascade。setTextureArray 会按 _refCount 给数组内 texture 加减引用,说明语义上 texture array 是 counted 的;但 cloneTo 对数组只 slice(),没有给 counted target 加引用,_addReferCount 也只处理 property instanceof Texture,不处理 Texture[]。这和本 PR 明确建立的 ShaderData/Material 资源计数契约不自洽。建议抽一个 helper 统一处理 Texture | Texture[],并补 set before host refCount > 0cloneTo counted target 两类测试。

[P3] Deep / Assignment 的 API 注释还有过度承诺。defaultCloneMode(Deep) 注释写成所有 Deep 类型必须无参构造,但当前设计允许 host-bound Deep 类型通过 preset clone,preset-less 时具名报错;建议改成「preset-less Deep clone 需要无参构造;host-bound/preset-only 类型无 preset 时会失败」。CloneMode.Assignment 注释写 ref-counted resource 会被 clone keep alive,但实际只在 component top-level slot settlement 做计数,nested level 不计数,也建议写明限定。

[P3] git diff --check 目前不干净:docs/en/core/clone.mdx 64/67 和 docs/zh/core/clone.mdx 65/68 有 trailing whitespace。

验证过核心相关测试:
pnpm exec vitest run tests/src/core/CloneManager.test.ts tests/src/core/CloneTextureRefCount.test.ts tests/src/core/RefCountContract.test.ts tests/src/core/ShaderDataRefCount.test.ts -> 86 passed。

…ract

setTextureArray counts its entries by the host's refCount, but the
cascade in _addReferCount only handled single textures and cloneTo
sliced arrays without acquiring — texture-array entries fell out of the
contract this PR pins for ShaderData. A shared helper now applies the
same cascade to Texture and Texture[]; covered by cascade and clone
lifecycle tests.

Also narrows two over-promising docs (Deep bare-construction scoped to
preset-less clones; Assignment keep-alive scoped to top-level slots)
and strips trailing whitespace in the clone guides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

…assignment

# Conflicts:
#	packages/core/src/animation/Animator.ts
#	packages/core/src/particle/modules/ParticleCompositeCurve.ts
#	tests/src/core/particle/ParticleCurve.test.ts
GuoLei1990

This comment was marked as outdated.

cptbtptpbcptdtptp and others added 2 commits July 10, 2026 20:59
…fault

Upstream galacean#3049 guaranteed orbital curve/offset cloning with @deepClone;
the merge dropped those decorators as redundant under type-driven Deep.
Pin the equivalence with identity + value assertions so a regression in
the type default surfaces here instead of in particle rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

增量审查 @ ec48059110(第十四轮)— delta = 2 commits(test-only:给 merge-in 的 orbital VOL 字段补 deep-clone 回归网 + 自修坏测试)

总结

自上轮 7ba6d16f3 起 tip 线性推进 2 个 commitcompare 返回 ahead_by:2 / behind_by:0,status ahead,merge_base=7ba6d16f3 且是新 tip 的直接祖先,无 force-push):

  1. 7027eecd3 test(clone): pin upstream orbital fields deep-cloning via the type default
  2. ec4805911 test(clone): fix orbital pin to use the public centerOffset accessor

只碰 1 文件 tests/src/core/CloneManager.test.ts(净 +19/-0)。给上一轮 merge-in 的 #3049(orbital/radial VOL)新增字段补一条 deep-clone 回归测试,commit 2 是 commit 1 的同周期自修——把 commit 1 写的 vol.offset(不存在的公开 getter)改成真实存在的公开访问器 vol.centerOffset本轮 CI 全绿(含 codecov job = vitest 实跑通过),实证测试正确落地。无 P0/P1/P2/P3,可合并。

逐链核对(delta 本身,非信 commit message)

1. 测试机制 — 真反向可证伪,走公开 API

  • setup:renderer.generator.velocityOverLifetimevol.orbitalX = new ParticleCompositeCurve(1, 2)constantMin=1/constantMax=2 重载核实存在,ParticleCompositeCurve.ts:128)+ vol.centerOffset.set(3,4,5)centerOffset getter 返回 live _offset Vector3,.set() 原地改,VelocityOverLifetimeModule.ts:202-204)。
  • gate 判定逐字段核实(对照 tip 树,非本地 worktree):
    • _orbitalXVelocityOverLifetimeModule.ts:80)——无字段装饰器 → 落类型默认;类型 ParticleCompositeCurve = @defaultCloneMode(CloneMode.Deep)ParticleCompositeCurve.ts:11)→ 深拷。断言 clonedVol.orbitalX !== vol.orbitalX(新实例)+ constantMin === 1 / constantMax === 2(值保留)。若 gate 误把它当 Assignment 共享引用 → !== FAIL,反向可证伪。
    • _offset:84)——无字段装饰器 → 落类型默认;Vector3CloneManager.ts:344-364_markDeep 块里注册为 Deep(math 值类型不能依赖 core,故在此集中注册)→ 深拷。断言 clonedVol.centerOffset !== vol.centerOffset(新实例)+ .z === 5(值保留)。反向可证伪同上。
  • rootEntity.destroy() 收尾,无残留污染共享 scene。这条测试把「#3049 新增的 orbital 曲线字段 + centerOffset 走 type-driven gate 正确深拷」钉成回归网——正是本 PR type-driven 机制的价值验证点:新字段作者不用再逐个标 @deepClone,靠类型默认自动 Deep,测试守住这个契约不被未来回归。

2. commit 2 是同周期自修(非独立缺陷)

  • commit 1 写的是 vol.offset.set(...) / expect(clonedVol.offset)...,但 VelocityOverLifetimeModule 无公开 offset getter——公开访问器只有 centerOffset(私有字段是 _offset)。故 commit 1 那版 vol.offsetTypeError: Cannot read properties of undefined (reading 'set')
  • commit 2(ec4805911 fix orbital pin to use the public centerOffset accessor)把 3 处 offsetcenterOffset 收口到真实公开 API。这既修了 commit 1 的坏测试、又回到「链路测试走公开访问器」的金标准。两 commit 合起来净 +19/-0(commit 2 是 3 行改名替换非新增)。CI 全绿实证收敛成功。

已核对为「已解决 / 不适用 / 不重提」

  • 本 delta 未触碰任何 clone 系统逻辑文件CloneManager.ts / CloneMode.ts / ShaderData.ts / SkinnedMeshRenderer.ts / Signal.ts / 两 clone.mdx 全 byte-unchanged),故历史全部闭环项(type-driven gate、_transferSlotOwnership、copyFrom 重排+硬化、TypedArray reuse !== value、joint texture 泄漏、_isShapeAttached 双挂上移、Map/Set for-of perf、bare-construction 崩溃修复 + guard 测试、_bareConstruct 诊断 funnel、host-bound-in-container 语义钉桩、texture-array refCount cascade)——不重提。
  • P3 bacause 拼写(clone.mdx)——本 delta 未触碰该文件,落在未改动行,已提过一次,不再重提。
  • ParticleCurve key-listener / ColliderShape PhysicsMaterial orphan(dev/2.0 既有 out-of-scope)——不重提。
  • 自上轮 7ba6d16f3(12:57)以来无新的 PR 评论 / inline comment / 作者回复需要 reconcile。我的 latestReviews state=COMMENTED(十四轮全 --comment,从无 state-changing CHANGES_REQUESTED),无遗留门控;reviewDecision=REVIEW_REQUIRED 是 repo 要求 owner 审、非我的门控。

注释合规

  • 新增 +19 行全部是断言/setup,零注释——无末尾句号 / aspirational / 复述代码等可查项。无偏离。

简化建议

无。delta 是一次干净的回归网补齐:给上一轮 merge-in 的 orbital VOL 新字段钉上「无装饰器字段靠类型默认自动深拷」的反向可证伪测试,并在同周期把首版误引用的私有形态 offset 修正为公开访问器 centerOffset。方向正确,把 type-driven 机制在 #3049 新字段上的正确性固化成 CI 护栏。可合并。

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

增量审查 @ ec4805911(第十四轮)— delta = 2 个 test-only commit,另修正第十三轮一处错误结论

总结

自第十三轮审查的 merge 7ba6d16f3 起 tip 线性推进 2 个 commit:7027eecd3(orbital 字段深拷等价性钉子)+ ec4805911(用例修正:offset → 公开访问器 centerOffset)。测试本身干净:全链路公开 API(addComponent → clone → getter),独立性(!==)+ 值断言双保险,反向可证伪(revert 类型级 Deep 注册则 not.eq 失败)。1 个 P2(针对 merge commit 的可追溯性,非本 delta 代码)+ 1 个 P3,不阻塞合并。

修正第十三轮结论:merge 7ba6d16f3 并非「零 clone 系统改动 / byte-identical」

第十三轮按「PR 所有权文件清单」比对宣布 merge 零改动——该清单漏掉了 merge 对上游侧文件的本地语义修改(evil-merge 面)。git show 7ba6d16f3(combined diff,只显示与两个 parent 都不同的行)实证三处:

  1. Animator.ts — 删除 #3024 新增的 @assignmentClone fireEvents。该装饰器 use-without-import(本分支已删该文件的 import),不删会在 shader-compiler precompile 时抛 ReferenceError;布尔原始值在新机制下天然按值拷,删除行为等价。
  2. VelocityOverLifetimeModule.ts — 删除 #3049 新增的 5 个 @deepClone(orbital 曲线 ×4 + _offset)。ParticleCompositeCurve / Vector3 类型级 Deep 兜底,等价,且与同文件 _velocityX/Y/Z 已删装饰器的口径一致。
  3. ParticleCompositeCurve.ts — 丢弃上游给 _updateManager 补的 @ignoreClone(UpdateFlagManager 类型级 Ignore 兜底,等价)。

等价性有据:本 delta 的 orbital pin 测试正是第 2 处的行为钉子;全量 1586/1586 绿。

问题

[P2] merge commit 7ba6d16f3 的三处上游装饰器删除无任何 message 记录 — merge message 是默认的 "Merge remote-tracking branch…"。上游 #3024/#3049 的作者日后 blame fireEvents / _orbitalX 行,会看到自己刚加的装饰器消失在一个无说明的 merge 里,无法区分「有意的机制等价清理」与「冲突解决误删」;第十三轮 review 被它瞒过(宣布 byte-identical)即是现实伤害的第一例。merge 已推送不宜改写历史,建议在 PR body 增补一段 merge 决策记录(三处删除 + 等价性依据 + 对应测试),供合并后追溯。

[P3] orbital pin 用例的 describe 归属 — 用例走组件字段(preset)路径,却放在 "Parameter-constructed Deep values as container elements" describe 下,describe 名与用例内容不符。建议挪到粒子引擎路径分组或调整分组命名。

已关闭问题核对

  • 注释句号形态(完整句带句号 vs 纯标签不带)— 第七轮已显式裁决「两种形态并存自洽」,按已关闭处理,不重提。
  • 历史闭环项(bare-construction 契约 + guard、host-bound 容器语义钉、TextureArray refCount cascade、copyFrom 硬化、TypedArray identity、槽位所有权契约等)— 本 delta 未触碰,不重提。

简化建议

无。两个 test-only commit 各自单一职责,符合链路测试原则。

The decorator is public API (37 engine classes use it) and the removal
of shallowClone pushes users toward type registration, yet the guide
only covered field decorators. Adds a section with the registration
example, the Assignment fallback motivation, and the bare-construction
contract for Deep types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator Author

Clone 性能基准 — base 75af66fb2 vs PR ec4805911

协议:独立 git worktree 各自构建 dist(避免产物污染);vitest 浏览器环境(headless chromium);每场景先预热再计时,每轮 1000 次克隆(树场景 100 次),3 轮取均值 ± 标准差;完整套独立跑了 2 遍(下表列全)。机器:本地 macOS(Apple Silicon)。

场景 base(两遍) PR(两遍) 合并 Δ
粒子实体(ParticleRenderer,语义等价) 106.9 ±7.9 / 109.1 ±7.3 µs 117.1 ±4.5 / 122.9 ±7.1 µs ≈ +11%
实体树 121 节点(MeshRenderer + Script,语义等价) 2781 ±83 / 2807 ±33 µs 2631 ±17 / 3051 ±38 µs ≈ +2%(小于轮间波动)
容器脚本(数组 1000 项 + Map 100 项 + 嵌套对象) 27.4 / 29.1 µs(共享,不拷) 42.9 / 45.0 µs(深拷) +16 µs = 语义变更成本

判读

  • 实体树(最接近真实 prefab 实例化的形态):Δ≈2%,小于轮间波动,无回归
  • 粒子实体:约 +11%(绝对 ~11 µs/clone)。来源:模块 preset 深拷路径上每字段的判定链(装饰器查询 → 容器判定 → 类型默认)比旧机制单次 cloneModes[k] 查询略贵。量级感:每帧克隆 8 个粒子系统实体才累计 1 ms。可用 per-class 字段计划缓存(首次克隆烘焙判定结果)追回,但会给刚简化完的机制加回复杂度,建议真实场景出现瓶颈时再做。
  • 容器脚本一行不是回归对比:base 语义是共享引用(克隆体与源指向同一数组/Map,一处修改两边可见),PR 语义是独立深拷。+16 µs 是正确性的直接成本,绝对值极小。
bench 脚本(未入库,两侧 worktree 各放一份运行)
class TypicalScript extends Script {
  speed = 1.5; label = "bench"; flag = true;
  @deepClone offset = new Vector3(1, 2, 3);
}
class ContainerScript extends Script {
  config = { numbers: new Array(1000).fill(0).map((_, i) => i), nested: { a: 1, b: "x", v: [1, 2, 3] } };
  lookup = new Map(Array.from({ length: 100 }, (_, i) => [`k${i}`, i]));
}
// 场景 1:实体 + ParticleRenderer,clone().destroy() ×1000×3 轮(预热 100)
// 场景 2:4 层 ×3 子 = 121 节点树,每节点 MeshRenderer + TypicalScript,×100×3 轮(预热 20)
// 场景 3:实体 + ContainerScript,×1000×3 轮(预热 100)
// 计时 performance.now(),输出 µs/clone 均值 ± 标准差

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

增量审查 @ cd3bad1dc(第十五轮)— delta = 1 commit(docs-only:为 @defaultCloneMode 补自定义类型注册指南)

总结

自上轮 ec4805911 起 tip 线性推进 1 个 commitcompare 返回 ahead_by:1 / behind_by:0,status ahead,merge_base=ec4805911 且是新 tip 的直接祖先,无 force-push):cd3bad1dc docs(clone): document defaultCloneMode registration for custom types —— 只碰 2 文件(docs/en/core/clone.mdx +20/-0、docs/zh/core/clone.mdx +20/-0,两语种镜像)。给 clone 指南补一节「为自定义类型注册克隆语义」,含注册示例 + Assignment 兜底动机 + Deep 类型的 bare-construction 契约告警。零产物代码改动,CI 全绿(build×3 macos/ubuntu/windows / e2e×4 / codecov+patch+project / lint / labeler)。无 P0/P1/P2/P3,可合并。

逐条核对(新增 prose 对照 tip 代码,非信 commit message)

新增段落共四条事实性论断,逐一对照 CloneManager.ts@tip(经 gh api contents?ref=<tip> 拉取真 PR tree,非本地 worktree)核实全部准确、非 aspirational

  1. @defaultCloneMode 是作用于类型的公开装饰器 —— CloneManager.ts:54-58 export function defaultCloneMode(mode)target.prototypedefineProperty("_defaultCloneMode")。经 packages/core/src/index.ts:72 export * from "./clone/CloneManager" + :73 CloneMode 双双透出,再经 packages/galacean/src/index.ts:9/11@galacean/engine-core + @galacean/engine-math 一并 re-export → 文档 import import { CloneMode, defaultCloneMode, Vector3 } from "@galacean/engine" 三个符号全可达、import 语句合法

  2. 注册把默认从 Assignment 翻成 Deep;字段装饰器仍可覆盖 —— :141-146 优先级链「field decorator (highest) → container default Deep → type's @defaultCloneMode → Assignment」。文档「未注册的类实例默认共享引用(Assignment)—— 注册正是让它变独立拷贝的开关」对应 :146 _defaultCloneMode ?? CloneMode.Assignment;「字段装饰器依然可以覆盖注册的默认值」对应 fieldMode 先判(:142 cloneMode === undefined 才落类型默认)。准确

  3. Deep 类型必须无参可构造;容器里 new Type() 后逐字段填充;带必需参数的构造函数在克隆时报具名错误 —— 这是我第八~十二轮反复钉桩过的 _bareConstruct 契约::332-343 try { new ctor() } catch { throw "CloneManager: failed to bare-construct \"<T>\" — a type cloned deep must support argument-less construction ..." }。触发面 scope 到「容器(array/Map/plain object)」也精确——:321 object 分支仅在 reusable=null 时走 _bareConstruct,而容器元素恒以 _cloneValue(v, undefined, ...)reuse=undefinedreusable=null;反之作为组件具名字段 slot 时复用同型构造 preset(reusable 非空)永不 bare。文档告警把失败面限定在「容器里遇到」正是这条机制的准确表述。准确

  4. <Callout type="warning"> 是既有 MDX 组件 —— 全仓 grep 命中 docs/{en,zh}/animation/layer.mdx / graphics/2D/*.mdx / graphics/camera/texture.mdx 等多处,非杜撰组件。

注释合规

  • 新增 20 行均为 .mdx markdown 正文 + 代码块 + <Callout>,非源码注释 —— 单行 // 句号规则 / JSDoc 规则不适用。正文为完整句带终止标点,符合 markdown 惯例。示例代码块内 base = 10 / falloff = new Vector3(...) 是演示字段无注释。无偏离。

已核对为「已解决 / 不适用 / 不重提」

  • 本 delta 未触碰任何 clone 系统逻辑文件CloneManager.ts / CloneMode.ts / ShaderData.ts / SkinnedMeshRenderer.ts / Signal.ts / CloneManager.test.ts 全 byte-unchanged),故历史全部闭环项(type-driven gate、_transferSlotOwnership、copyFrom 重排+硬化、TypedArray reuse !== value、joint texture 泄漏、_isShapeAttached 双挂上移、Map/Set for-of perf、bare-construction 崩溃修复 + guard 测试、_bareConstruct 诊断 funnel、host-bound-in-container 语义钉桩、texture-array refCount cascade、orbital VOL deep-clone 测试)——不重提。
  • P3 bacause 拼写(clone.mdx)—— 本 delta 从行 148 起新增,bacause 那行(:145)是 diff 的 context 行、未改动,落在未改动行且已提过一次,不再重提。
  • ParticleCurve key-listener / ColliderShape PhysicsMaterial orphan(dev/2.0 既有 out-of-scope)——不重提。
  • 自上轮 ec4805911(13:08)以来唯一新评论是作者 14:16 的 clone 性能基准(base vs PR:实体树 Δ≈2% 无回归 / 粒子实体 +11% ~11µs/clone / 容器脚本 +16µs=共享→深拷的语义变更成本)——信息性数据、非 finding 或问题,无需 reconcile。我的 latestReviews state=COMMENTED(十五轮全 --comment,从无 state-changing CHANGES_REQUESTED),无遗留门控;reviewDecision=REVIEW_REQUIRED 是 repo 要求 owner 审、非我的门控。

简化建议

无。delta 是一次干净的文档补齐:shallowClone 移除后用户被推向类型级注册,而旧指南只讲字段装饰器 —— 本节补上「@defaultCloneMode 注册 + Assignment 兜底动机 + Deep 类型无参构造契约」这块缺口,四条论断逐一对照 tip 代码核实准确、import 合法、组件既有、两语种镜像一致。方向正确,把 type-driven 机制面向自定义类型作者的公开契约文档化了。可合并。

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

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants