Skip to content

feat: support multiple masks#1482

Open
ChengYi996 wants to merge 4 commits into
feat/2.10from
feat/multiple-mask
Open

feat: support multiple masks#1482
ChengYi996 wants to merge 4 commits into
feat/2.10from
feat/multiple-mask

Conversation

@ChengYi996

@ChengYi996 ChengYi996 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary
Add support for multiple mask references per component, replacing the previous single-mask model. A component can now be clipped by multiple masks simultaneously (intersection of forward masks, with optional inverted masks to punch holes).

Core Changes (effects-core)

  • MaskProcessor (mask-ref-manager.ts): Refactored to manage a list of MaskReference[] instead of a single maskable.
  • Supports forward/inverted masks, deduplication, reference count capping (254 max), and per-frame stencil rendering with multi-pass write.
  • setMaskOptions: Accepts the new references[] array format. Resolves mask paths via engine.findObject, warns on missing references, and deduplicates by maskable identity.
  • Migration (migration.ts): Converts legacy formats to the new references[] structure:
  • Old renderer.maskMode (MASK/OBSCURED/REVERSE_OBSCURED) → resolved via sequential traversal with currentMaskComponentId
  • Old mask.reference + mask.inverted → wrapped into references[{ mask, inverted }]
  • Backward compatibility: MaskMode enum and setMaskMode() function retained with @deprecated 2.10 annotations — old users importing these symbols will not get compile errors on upgrade.
  • Dead code cleanup: Removed unused mask/maskMode fields from ParticleMeshProps, TrailMeshProps (were written but never read).

Spine Plugin (plugin-packages/spine)

  • SpineComponent.rendererOptions: Type updated to match runtime reality (flat structure). Added mask?/maskMode? as optional @deprecated 2.10 fields for backward compatibility.
  • SpineMesh / SlotGroup: Removed the dead renderOptions pass-through chain entirely (SpineMeshRenderInfo.renderOptions → SlotGroup.renderOptions → constructor params → createMaterial args were all unused).

Demo & Tests

  • New multi-mask demo (web-packages/demo): 14 interactive cases covering JSON declarations (single/intersection/inverted/empty/missing-ref/dedup), legacy format migration, and dynamic API (addMaskReference/removeMaskReference/clearMaskReferences). IDs resolved dynamically by item name for portability across different JSON files.
  • Unit tests (mask-processor.spec.ts): Added tests for setMaskOptions with references array, deduplication, stencil limit capping, and re-initialization.

Summary by CodeRabbit

  • New Features

    • Multi-mask support enabling multiple mask references per object.
    • New interactive multi-mask demo with runtime add/remove/clear and legacy-compat cases.
  • Deprecations

    • Legacy single-mask/mode API deprecated; migrate to the multi-mask reference model.
  • Bug Fixes

    • Improved stencil/material state preservation during mask rendering and safer handling of missing/duplicate mask references.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05137c7b-d9eb-4091-88c6-8d1a21ed021c

📥 Commits

Reviewing files that changed from the base of the PR and between 8a974cf and 96e22a0.

📒 Files selected for processing (1)
  • packages/effects-core/src/material/mask-ref-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/effects-core/src/material/mask-ref-manager.ts

📝 Walkthrough

Walkthrough

Refactors runtime masking from a single MaskMode enum to a MaskProcessor multi-reference API; updates fallback migration, components, plugins, tests, and adds a multi-mask demo.

Changes

Multi-Mask Reference System Migration

Layer / File(s) Summary
MaskProcessor multi-mask reference API
packages/effects-core/src/material/mask-ref-manager.ts, packages/effects-core/src/material/types.ts, packages/effects-core/src/material/utils.ts
New MaskOptions/MaskOptionReference types and MAX 254 reference cap; setMaskOptions resolves references via engine.findObject, deduplicates by maskable, and warns on conflicts; getMaskReferences() added; per-call stencil state save/restore and removal of MaskMode fields; MaskMode enum documented deprecated.
Legacy mask format migration and normalization
packages/effects-core/src/fallback/migration.ts
version36Migration and content processing now reset currentMaskComponentId, process composition items robustly, and convert legacy numeric renderer.maskMode (NONE/MASK/OBSCURED) and single-mask mask.reference/mask.inverted into the new renderContent.mask.references[] shape with warnings for missing obscured masks.
Base component mask API integration
packages/effects-core/src/components/base-render-component.ts
Removes MaskMode import and maskMode shader binding; deprecates ItemRenderer.mask in docs; fromData uses maskManager.isMask for transparentOcclusion and sets mask: 1 constant instead of mask ref value.
Shape component and particle system mask cleanup
packages/effects-core/src/plugins/shape/shape-component.ts, packages/effects-core/src/plugins/particle/particle-mesh.ts, packages/effects-core/src/plugins/particle/particle-system.ts, packages/effects-core/src/plugins/particle/trail-mesh.ts
Shape component removes maskMode usage and _TexParams.w assignment; particle mesh/trail types drop mask/maskMode fields; ParticleSystem.fromData stops populating mask-related fields for particle/trail props.
Spine plugin renderOptions and mask removal
plugin-packages/spine/src/spine-component.ts, plugin-packages/spine/src/slot-group.ts, plugin-packages/spine/src/spine-mesh.ts
SpineComponent.rendererOptions type changed to optional deprecated mask/maskMode; SlotGroup no longer stores/passes renderOptions; SpineMesh constructor and createMaterial() no longer accept mask-related parameters.
Multi-mask demo implementation and showcase
web-packages/demo/src/multi-mask.ts, web-packages/demo/html/multi-mask.html
New demo with embedded scene JSON; exercises multi-reference masking (intersection, inversion, empty/missing refs, legacy format) and dynamic maskManager operations via UI test cases.
Mask processor unit test expansion
web-packages/test/unit/src/effects-core/mask-processor.spec.ts
Removes MaskMode import; tests updated to cover isMask, references parsing, invalid reference skipping, 254-reference cap, deduplication, alphaMaskEnabled, stencil state restoration (including stencilOpFail/stencilOpZFail), and frameClipMasks handling.
Demo navigation and single.ts refactoring
web-packages/demo/index.html, web-packages/demo/src/single.ts
Adds multi-mask demo to index; single.ts now loads remote scene URLs sequentially, disposes prior composition, awaits end events, and uses simplified error logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • yiiqii
  • wumaolinmaoan

Poem

🐰 In code the masks unspool and bloom,

Arrays replace one enum's lone room;
Two-fifty-four seats in a layered dance,
References hop in, give masking a chance! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support multiple masks' accurately and concisely describes the primary change: adding support for multiple mask references per component, which is the main feature introduced across all affected files.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multiple-mask

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/effects-core/src/components/base-render-component.ts (2)

321-325: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset the mask manager when data.mask is absent.

fromData() only updates mask state inside the if (maskOptions) branch. On re-initialization from masked data to unmasked data, the previous references/flags stay live, so this component can keep stale stencil and alpha-mask behavior. Push an explicit empty mask config here, or call the clear/reset path before rebuilding this.renderer.

Proposed fix
     const maskOptions = data.mask;

-    if (maskOptions) {
-      this.maskManager.setMaskOptions(this.engine, maskOptions);
-    }
+    if (maskOptions) {
+      this.maskManager.setMaskOptions(this.engine, maskOptions);
+    } else {
+      this.maskManager.setMaskOptions(this.engine, { references: [] });
+    }
🤖 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/effects-core/src/components/base-render-component.ts` around lines
321 - 325, fromData() only sets mask state when maskOptions exists, leaving
previous mask live when data.mask is absent; update the branch so that when
maskOptions is falsy you explicitly clear or reset the mask before rebuilding
the renderer—e.g. call a clear/reset API (maskManager.clearMask or similar) or
invoke this.maskManager.setMaskOptions(this.engine, {}) prior to recreating
this.renderer so stale stencil/alpha-mask flags and references are removed;
modify the block around maskOptions and this.maskManager.setMaskOptions to
handle the absent-mask case.

327-335: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hardcode the deprecated renderer.mask to a truthy value.

Lines 22-25 say this field is still kept for compatibility reads, but Line 334 makes every MaskableGraphic look masked even when data.mask is missing. Any legacy consumer that still branches on renderer.mask will now misclassify unmasked components after upgrade. Derive it from the actual mask state instead of a constant.

Proposed fix
+    const hasMaskState =
+      this.maskManager.isMask || this.maskManager.getMaskReferences().length > 0;
+
     this.renderer = {
       renderMode: renderer.renderMode ?? spec.RenderMode.MESH,
       blending: renderer.blending ?? spec.BlendingMode.ALPHA,
       texture: renderer.texture ? this.engine.findObject<Texture>(renderer.texture) : this.engine.whiteTexture,
       occlusion: !!renderer.occlusion,
       transparentOcclusion: !!renderer.transparentOcclusion || this.maskManager.isMask,
       side: renderer.side ?? spec.SideMode.DOUBLE,
-      mask: 1,
+      mask: hasMaskState ? 1 : 0,
     };
🤖 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/effects-core/src/components/base-render-component.ts` around lines
327 - 335, The deprecated renderer.mask should not be hardcoded to 1; instead
derive it from the real mask state so legacy consumers are preserved. In the
this.renderer initialization (the object built inside base-render-component),
set mask to the existing renderer.mask when provided, otherwise compute it from
the actual mask state (e.g., use renderer.mask ?? (this.maskManager.isMask ? 1 :
0) or an equivalent truthy/falsey numeric value) so MaskableGraphic and any
consumers that check renderer.mask behave correctly.
🧹 Nitpick comments (1)
web-packages/test/unit/src/effects-core/mask-processor.spec.ts (1)

373-390: ⚡ Quick win

Align the test name with asserted behavior.

Line 373 says this case verifies both warning and capping, but the assertions only validate capping. Please either assert the warning path (e.g., spy on warn) or rename the case to avoid overclaiming coverage.

🤖 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 `@web-packages/test/unit/src/effects-core/mask-processor.spec.ts` around lines
373 - 390, The test title overclaims by mentioning a warning but only asserts
capping; update the test description to match the asserted behavior (e.g.,
change the it(...) title from "should warn and cap when mask references exceed
the stencil limit" to "should cap mask references to the stencil limit when
exceeding it") or alternatively add an assertion that verifies the warning path
by spying on the logger/warn before calling MaskProcessor.setMaskOptions;
references to help locate code: MaskProcessor, mp (the instance),
setMaskOptions, and getMaskReferences.
🤖 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/effects-core/src/material/mask-ref-manager.ts`:
- Around line 256-257: copyStencilArrayValue currently no-ops when the source
stencil arrays are undefined, which leaves previous stencil state in
prevStencilOpFail/prevStencilOpZFail (and the other prevStencil* arrays) and
causes state leakage across materials; change copyStencilArrayValue (the
function defined around lines 323-326) so that if the source is undefined it
explicitly clears the destination (e.g., set dest.length = 0 or set dest to
undefined/empty) rather than returning early, and keep the existing call sites
(the lines that copy into prevStencilOpFail/prevStencilOpZFail and the other
prevStencil* variables) so they will now correctly clear stale values when
material.stencilOpFail or material.stencilOpZFail (and the other stencil fields)
are undefined.

---

Outside diff comments:
In `@packages/effects-core/src/components/base-render-component.ts`:
- Around line 321-325: fromData() only sets mask state when maskOptions exists,
leaving previous mask live when data.mask is absent; update the branch so that
when maskOptions is falsy you explicitly clear or reset the mask before
rebuilding the renderer—e.g. call a clear/reset API (maskManager.clearMask or
similar) or invoke this.maskManager.setMaskOptions(this.engine, {}) prior to
recreating this.renderer so stale stencil/alpha-mask flags and references are
removed; modify the block around maskOptions and this.maskManager.setMaskOptions
to handle the absent-mask case.
- Around line 327-335: The deprecated renderer.mask should not be hardcoded to
1; instead derive it from the real mask state so legacy consumers are preserved.
In the this.renderer initialization (the object built inside
base-render-component), set mask to the existing renderer.mask when provided,
otherwise compute it from the actual mask state (e.g., use renderer.mask ??
(this.maskManager.isMask ? 1 : 0) or an equivalent truthy/falsey numeric value)
so MaskableGraphic and any consumers that check renderer.mask behave correctly.

---

Nitpick comments:
In `@web-packages/test/unit/src/effects-core/mask-processor.spec.ts`:
- Around line 373-390: The test title overclaims by mentioning a warning but
only asserts capping; update the test description to match the asserted behavior
(e.g., change the it(...) title from "should warn and cap when mask references
exceed the stencil limit" to "should cap mask references to the stencil limit
when exceeding it") or alternatively add an assertion that verifies the warning
path by spying on the logger/warn before calling MaskProcessor.setMaskOptions;
references to help locate code: MaskProcessor, mp (the instance),
setMaskOptions, and getMaskReferences.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0f1b8a4-dd5c-4174-9ebd-c7693594bbb2

📥 Commits

Reviewing files that changed from the base of the PR and between 19ecdd3 and 8a974cf.

📒 Files selected for processing (17)
  • packages/effects-core/src/components/base-render-component.ts
  • packages/effects-core/src/fallback/migration.ts
  • packages/effects-core/src/material/mask-ref-manager.ts
  • packages/effects-core/src/material/types.ts
  • packages/effects-core/src/material/utils.ts
  • packages/effects-core/src/plugins/particle/particle-mesh.ts
  • packages/effects-core/src/plugins/particle/particle-system.ts
  • packages/effects-core/src/plugins/particle/trail-mesh.ts
  • packages/effects-core/src/plugins/shape/shape-component.ts
  • plugin-packages/spine/src/slot-group.ts
  • plugin-packages/spine/src/spine-component.ts
  • plugin-packages/spine/src/spine-mesh.ts
  • web-packages/demo/html/multi-mask.html
  • web-packages/demo/index.html
  • web-packages/demo/src/multi-mask.ts
  • web-packages/demo/src/single.ts
  • web-packages/test/unit/src/effects-core/mask-processor.spec.ts
💤 Files with no reviewable changes (4)
  • packages/effects-core/src/plugins/particle/particle-mesh.ts
  • plugin-packages/spine/src/slot-group.ts
  • packages/effects-core/src/plugins/particle/trail-mesh.ts
  • packages/effects-core/src/plugins/particle/particle-system.ts

Comment thread packages/effects-core/src/material/mask-ref-manager.ts Outdated
export function processContent (composition: spec.CompositionData) {
//@ts-expect-error
for (const item of composition.items) {
const items = composition.items;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

这个数据转换要在新的 migration 函数上改。不然老版本数据会和编辑器发的对不上

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