refactor(clone): type-driven default clone mode for container elements#3040
refactor(clone): type-driven default clone mode for container elements#3040cptbtptpbcptdtptp wants to merge 59 commits into
Conversation
…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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe 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. ChangesClone pipeline and core remapping
Clone-mode updates across engine, particle, physics, and UI classes
Clone and ref-count test coverage
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
packages/core/src/clone/CloneManager.tspackages/core/src/clone/ComponentCloner.tspackages/core/src/shader/state/BlendState.tspackages/core/src/shader/state/DepthState.tspackages/core/src/shader/state/RasterState.tspackages/core/src/shader/state/RenderState.tspackages/core/src/shader/state/RenderTargetBlendState.tspackages/core/src/shader/state/StencilState.ts
…+ 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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
|
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. 2. Remap is unified through the identity map and field decorators win at every depth. The Also in this push: Known pre-existing (not addressed): 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 🤖 Generated with Claude Code |
…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>
…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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cptbtptpbcptdtptp
left a comment
There was a problem hiding this comment.
发现一个可复现的 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 值。
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>
…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>
…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>
cptbtptpbcptdtptp
left a comment
There was a problem hiding this comment.
最后一轮看 API / 注释 / 设计自洽性,整体方向我认可:类型驱动默认行为、容器默认结构 deep、未知对象 assignment、Entity/Component 统一走 identity-map remap,ref-count 从 clone gate 移到 component slot settlement,也比递归层里做 ownership 更清楚。
还有几个建议收口:
[P2] ShaderData 的 TextureArray 没进入 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 > 0、cloneTo 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>
…assignment # Conflicts: # packages/core/src/animation/Animator.ts # packages/core/src/particle/modules/ParticleCompositeCurve.ts # tests/src/core/particle/ParticleCurve.test.ts
…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
left a comment
There was a problem hiding this comment.
增量审查 @ ec48059110(第十四轮)— delta = 2 commits(test-only:给 merge-in 的 orbital VOL 字段补 deep-clone 回归网 + 自修坏测试)
总结
自上轮 7ba6d16f3 起 tip 线性推进 2 个 commit(compare 返回 ahead_by:2 / behind_by:0,status ahead,merge_base=7ba6d16f3 且是新 tip 的直接祖先,无 force-push):
7027eecd3test(clone): pin upstream orbital fields deep-cloning via the type defaultec4805911test(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.velocityOverLifetime→vol.orbitalX = new ParticleCompositeCurve(1, 2)(constantMin=1/constantMax=2重载核实存在,ParticleCompositeCurve.ts:128)+vol.centerOffset.set(3,4,5)(centerOffsetgetter 返回 live_offsetVector3,.set()原地改,VelocityOverLifetimeModule.ts:202-204)。 - gate 判定逐字段核实(对照 tip 树,非本地 worktree):
_orbitalX(VelocityOverLifetimeModule.ts:80)——无字段装饰器 → 落类型默认;类型ParticleCompositeCurve=@defaultCloneMode(CloneMode.Deep)(ParticleCompositeCurve.ts:11)→ 深拷。断言clonedVol.orbitalX !== vol.orbitalX(新实例)+constantMin === 1/constantMax === 2(值保留)。若 gate 误把它当 Assignment 共享引用 →!==FAIL,反向可证伪。_offset(:84)——无字段装饰器 → 落类型默认;Vector3在CloneManager.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无公开offsetgetter——公开访问器只有centerOffset(私有字段是_offset)。故 commit 1 那版vol.offset会TypeError: Cannot read properties of undefined (reading 'set')。 - commit 2(
ec4805911 fix orbital pin to use the public centerOffset accessor)把 3 处offset→centerOffset收口到真实公开 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 重排+硬化、TypedArrayreuse !== 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 未触碰该文件,落在未改动行,已提过一次,不再重提。 ParticleCurvekey-listener /ColliderShapePhysicsMaterial orphan(dev/2.0既有 out-of-scope)——不重提。- 自上轮
7ba6d16f3(12:57)以来无新的 PR 评论 / inline comment / 作者回复需要 reconcile。我的latestReviewsstate=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
left a comment
There was a problem hiding this comment.
增量审查 @ 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 都不同的行)实证三处:
Animator.ts— 删除 #3024 新增的@assignmentClone fireEvents。该装饰器 use-without-import(本分支已删该文件的 import),不删会在 shader-compiler precompile 时抛ReferenceError;布尔原始值在新机制下天然按值拷,删除行为等价。VelocityOverLifetimeModule.ts— 删除 #3049 新增的 5 个@deepClone(orbital 曲线 ×4 +_offset)。ParticleCompositeCurve/Vector3类型级 Deep 兜底,等价,且与同文件_velocityX/Y/Z已删装饰器的口径一致。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>
Clone 性能基准 — base
|
| 场景 | 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
left a comment
There was a problem hiding this comment.
增量审查 @ cd3bad1dc(第十五轮)— delta = 1 commit(docs-only:为 @defaultCloneMode 补自定义类型注册指南)
总结
自上轮 ec4805911 起 tip 线性推进 1 个 commit(compare 返回 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:
-
@defaultCloneMode是作用于类型的公开装饰器 ——CloneManager.ts:54-58export function defaultCloneMode(mode)在target.prototype上defineProperty("_defaultCloneMode")。经packages/core/src/index.ts:72export * from "./clone/CloneManager"+:73CloneMode双双透出,再经packages/galacean/src/index.ts:9/11把@galacean/engine-core+@galacean/engine-math一并 re-export → 文档 importimport { CloneMode, defaultCloneMode, Vector3 } from "@galacean/engine"三个符号全可达、import 语句合法。 -
注册把默认从 Assignment 翻成 Deep;字段装饰器仍可覆盖 ——
:141-146优先级链「field decorator (highest) → container default Deep → type's@defaultCloneMode→ Assignment」。文档「未注册的类实例默认共享引用(Assignment)—— 注册正是让它变独立拷贝的开关」对应:146_defaultCloneMode ?? CloneMode.Assignment;「字段装饰器依然可以覆盖注册的默认值」对应fieldMode先判(:142cloneMode === undefined才落类型默认)。准确。 -
Deep 类型必须无参可构造;容器里
new Type()后逐字段填充;带必需参数的构造函数在克隆时报具名错误 —— 这是我第八~十二轮反复钉桩过的_bareConstruct契约::332-343try { new ctor() } catch { throw "CloneManager: failed to bare-construct \"<T>\" — a type cloned deep must support argument-less construction ..." }。触发面 scope 到「容器(array/Map/plain object)」也精确——:321object 分支仅在reusable=null时走_bareConstruct,而容器元素恒以_cloneValue(v, undefined, ...)传reuse=undefined→reusable=null;反之作为组件具名字段 slot 时复用同型构造 preset(reusable非空)永不 bare。文档告警把失败面限定在「容器里遇到」正是这条机制的准确表述。准确。 -
<Callout type="warning">是既有 MDX 组件 —— 全仓grep命中docs/{en,zh}/animation/layer.mdx/graphics/2D/*.mdx/graphics/camera/texture.mdx等多处,非杜撰组件。
注释合规
- 新增 20 行均为
.mdxmarkdown 正文 + 代码块 +<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 重排+硬化、TypedArrayreuse !== 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 行、未改动,落在未改动行且已提过一次,不再重提。 ParticleCurvekey-listener /ColliderShapePhysicsMaterial 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。我的latestReviewsstate=COMMENTED(十五轮全--comment,从无 state-changing CHANGES_REQUESTED),无遗留门控;reviewDecision=REVIEW_REQUIRED是 repo 要求 owner 审、非我的门控。
简化建议
无。delta 是一次干净的文档补齐:shallowClone 移除后用户被推向类型级注册,而旧指南只讲字段装饰器 —— 本节补上「@defaultCloneMode 注册 + Assignment 兜底动机 + Deep 类型无参构造契约」这块缺口,四条论断逐一对照 tip 代码核实准确、import 合法、组件既有、两语种镜像一致。方向正确,把 type-driven 机制面向自定义类型作者的公开契约文档化了。可合并。
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
@deepClone/@assignmentClone/@ignoreClone(explicit per-field override, effective at every depth).@deepCloneon an engine-bound value can't be honored: Entity/Component refs fall back to remap, registered assets to sharing — each with a warning.@defaultCloneMode.Assignment(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/Component→ Remap — rewired to the clone within the cloned subtree via the identity map (single remap mechanism; the old path-walkCloneUtilsis removed).CloneManager(the math package can't depend on core's decorator); a completeness test guards that every math export withcopyFromis registered — it already caughtRay, which now implementsclone()/copyFrom().UpdateFlagManager,UpdateFlag,DisorderedArray,SafeLoopArray) → Ignore — the clone keeps its own constructor-built state; their former per-field@ignoreCloneannotations are gone.Ref-count model — slot-ownership contract
ReferResourcefamily; duck-typed counters likeShaderare excluded) owns one reference: the gate acquires it while cloning the slot (+1, and-1when 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.onDestroy), the same way engine components release in their destroy paths.Transition._cloneTo+1 ↔Transition.destroy−1, all four state slots counted in the base class (SpriteTransitioncarries no ref-count code).@ignoreCloneso the setter is the single+1source (MeshRenderer.mesh,Renderermaterials,MeshColliderShape.mesh).+1compensations inCamera/Animator/AudioSource_cloneToare removed — the gate acquisition replaces them.Changes
CloneModegainsIgnoreand is now exported.@assignmentClone/@ignoreCloneon Entity/Component refs are now honored literally (share / keep own) instead of being silently overridden by auto-remap.@defaultCloneMode.@shallowCloneis removed (breaking) — migrate with@deepCloneor@assignmentClone._registerCloneMapmerged into tree construction — two recursive walks instead of three);CloneUtils,Entity._remap,Component._remapand thesrcRoot/targetRootthreading are removed._cloneTohooks receive(target, cloneMap);Signalremaps listener targets/args by map lookup (Entity/Component args only, deterministically)._isContainerpoint shared by the gate and the deep-clone dispatch;DataViewclones 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.deepCloneObjecthonors field-level decorators like the rest of the system.ColliderShape(deep-cloned shapes own their native handles;MeshColliderShaperebuilds via the mesh setter, attaching exactly once — attachment state is tracked onColliderShape) and uiTransitionregistered@defaultCloneMode(Deep)— cloned colliders/interactives no longer share mutable shape/transition instances with the source.copyFromvalue-type dispatch runs after the container branches and only for class instances with a callablecopyFrom(a plain object carrying acopyFromdata key no longer crashes clone);SkinnedMeshRendererdrops 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/en|zh/core/clone.mdx, how-to-contribute) rewritten to the type-driven semantics:shallowCloneremoved 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/ uiText._font(destroy released a count the clone never acquired) andMeshColliderShape._mesh(same, plus the cloned shape silently lacked a native shape) were unbalanced ondev/2.0before this PR; the slot-ownership contract requires them fixed here to stay self-consistent. Destroying aParticleRendereralso never released aMeshShape's mesh — fixed via aBaseShape._destroyhook on the generator destroy chain.Test plan
copyFromcompleteness 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)bdcc6353— build ×3 platforms, lint, codecov (patch 96.3%), e2e visual regressions ×4 shardsMerge 决策记录 —
7ba6d16f3(dev/2.0 同步合并)本次同步合并在文本冲突解决之外,有意修改了三处上游代码行(可通过
git show 7ba6d16f3的 combined diff 查看)。三处删除在类型驱动闸门下均行为等价:Animator.ts— 删除 fix(animation): harden animator state instance data #3024 新增的fireEvents上的@assignmentClone。 该装饰器进入本分支时处于"使用但未导入"状态(本分支已删除该文件的对应 import),不删会在 shader-compiler 预编译时抛ReferenceError;布尔原始值在闸门下天然按值拷贝,装饰器冗余。VelocityOverLifetimeModule.ts— 删除 feat(particle): add orbital radial velocity over lifetime #3049 新增的 5 个@deepClone(orbital 曲线 ×4 +_offset)。ParticleCompositeCurve与Vector3经类型注册即深拷,与同文件已去装饰器的_velocityX/Y/Z口径一致。由测试 "orbital velocity fields deep-clone through the type default" 钉住。ParticleCompositeCurve.ts— 删除上游为_updateManager补的@ignoreClone。UpdateFlagManager已类型注册为 Ignore,字段装饰器冗余。🤖 Generated with Claude Code