[ts][pixi-v7] Aligned slot object mask to spine-pixi-v8 to fix non removed masks. See #2872.

This commit is contained in:
Davide Tantillo 2025-06-13 16:44:54 +02:00
parent e1c6b54312
commit 946868a04e
3 changed files with 69 additions and 46 deletions

View File

@ -127,7 +127,7 @@
// for clipping attachments having slot objects // for clipping attachments having slot objects
setTimeout(() => { setTimeout(() => {
spineboy.state.setAnimation(0, "portal", true) spineboy.state.setAnimation(0, "portal", true)
const tooth3 = PIXI.Sprite.from('assets/raptor-jaw-tooth.png'); const tooth3 = PIXI.Sprite.from('/assets/raptor-jaw-tooth.png');
tooth3.scale.set(2); tooth3.scale.set(2);
tooth3.x = -60; tooth3.x = -60;
tooth3.y = 120; tooth3.y = 120;

View File

@ -36,6 +36,7 @@ import {
Color, Color,
MeshAttachment, MeshAttachment,
Physics, Physics,
Pool,
RegionAttachment, RegionAttachment,
Skeleton, Skeleton,
SkeletonBinary, SkeletonBinary,
@ -323,7 +324,6 @@ export class Spine extends Container {
private lightColor = new Color(); private lightColor = new Color();
private darkColor = new Color(); private darkColor = new Color();
private clippingVertAux = new Float32Array(6);
private _boundsProvider?: SpineBoundsProvider; private _boundsProvider?: SpineBoundsProvider;
/** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */ /** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */
@ -441,8 +441,8 @@ export class Spine extends Container {
this.slotsObject.clear(); this.slotsObject.clear();
for (let maskKey in this.clippingSlotToPixiMasks) { for (let maskKey in this.clippingSlotToPixiMasks) {
const mask = this.clippingSlotToPixiMasks[maskKey]; const maskObj = this.clippingSlotToPixiMasks[maskKey];
mask.destroy(); maskObj.mask?.destroy();
delete this.clippingSlotToPixiMasks[maskKey]; delete this.clippingSlotToPixiMasks[maskKey];
} }
@ -575,14 +575,7 @@ export class Spine extends Container {
} }
private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
private clippingSlotToPixiMasks: Record<string, Graphics> = {}; private clippingSlotToPixiMasks: Record<string, SlotsToClipping> = {};
private pixiMaskCleanup (slot: Slot) {
let mask = this.clippingSlotToPixiMasks[slot.data.name];
if (mask) {
delete this.clippingSlotToPixiMasks[slot.data.name];
mask.destroy();
}
}
private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) { private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) {
const { container: slotObject, followAttachmentTimeline } = element const { container: slotObject, followAttachmentTimeline } = element
@ -615,30 +608,62 @@ export class Spine extends Container {
} }
} }
private updateAndSetPixiMask (pixiMaskSource: PixiMaskSource | null, pixiObject: Container) { private currentClippingSlot: SlotsToClipping | undefined;
if (Spine.clipper.isClipping() && pixiMaskSource) { private updateAndSetPixiMask (slot: Slot, last: boolean) {
let mask = this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] as Graphics; // assign/create the currentClippingSlot
const attachment = slot.attachment;
if (attachment && attachment instanceof ClippingAttachment) {
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() });
clip.maskComputed = false;
this.currentClippingSlot = clip;
return;
}
// assign the currentClippingSlot mask to the slot object
let currentClippingSlot = this.currentClippingSlot;
const slotObject = this.slotsObject.get(slot);
if (currentClippingSlot && slotObject) {
// create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot
let mask = currentClippingSlot.mask;
if (!mask) { if (!mask) {
mask = new Graphics(); mask = maskPool.obtain();
this.clippingSlotToPixiMasks[pixiMaskSource.slot.data.name] = mask; currentClippingSlot.mask = mask;
this.addChild(mask); this.addChild(mask);
} }
if (!pixiMaskSource.computed) {
pixiMaskSource.computed = true; // compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot
const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment; if (!currentClippingSlot.maskComputed) {
let slotClipping = currentClippingSlot.slot;
let clippingAttachment = slotClipping.attachment as ClippingAttachment;
currentClippingSlot.maskComputed = true;
const worldVerticesLength = clippingAttachment.worldVerticesLength; const worldVerticesLength = clippingAttachment.worldVerticesLength;
if (this.clippingVertAux.length < worldVerticesLength) this.clippingVertAux = new Float32Array(worldVerticesLength); const vertices = currentClippingSlot.vertices;
clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2); clippingAttachment.computeWorldVertices(slotClipping, 0, worldVerticesLength, vertices, 0, 2);
mask.clear().lineStyle(0).beginFill(0x000000); mask.clear().lineStyle(0).beginFill(0x000000).drawPolygon(vertices).endFill();
mask.moveTo(this.clippingVertAux[0], this.clippingVertAux[1]);
for (let i = 2; i < worldVerticesLength; i += 2) {
mask.lineTo(this.clippingVertAux[i], this.clippingVertAux[i + 1]);
}
mask.finishPoly();
} }
pixiObject.mask = mask;
} else if (pixiObject.mask) { slotObject.container.mask = mask;
pixiObject.mask = null; } else if (slotObject?.container.mask) {
// remove the mask, if slot object has a mask, but currentClippingSlot is undefined
slotObject.container.mask = null;
}
// if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined
if (currentClippingSlot && (currentClippingSlot.slot.attachment as ClippingAttachment).endSlot == slot.data) {
this.currentClippingSlot = undefined;
}
// clean up unused masks
if (last) {
for (const key in this.clippingSlotToPixiMasks) {
const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key];
if ((!(clippingSlotToPixiMask.slot.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
this.removeChild(clippingSlotToPixiMask.mask);
maskPool.free(clippingSlotToPixiMask.mask);
clippingSlotToPixiMask.mask = undefined;
}
}
this.currentClippingSlot = undefined;
} }
} }
@ -658,7 +683,7 @@ export class Spine extends Container {
let triangles: Array<number> | null = null; let triangles: Array<number> | null = null;
let uvs: NumberArrayLike | null = null; let uvs: NumberArrayLike | null = null;
let pixiMaskSource: PixiMaskSource | null = null;
const drawOrder = this.skeleton.drawOrder; const drawOrder = this.skeleton.drawOrder;
for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) { for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) {
@ -670,14 +695,13 @@ export class Spine extends Container {
if (pixiObject) { if (pixiObject) {
this.updateSlotObject(pixiObject, slot, zIndex + 1); this.updateSlotObject(pixiObject, slot, zIndex + 1);
slotObjectsCounter++; slotObjectsCounter++;
this.updateAndSetPixiMask(pixiMaskSource, pixiObject.container);
} }
this.updateAndSetPixiMask(slot, i === drawOrder.length - 1);
const useDarkColor = slot.darkColor != null; const useDarkColor = slot.darkColor != null;
const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
if (!slot.bone.active) { if (!slot.bone.active) {
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
continue; continue;
} }
const attachment = slot.getAttachment(); const attachment = slot.getAttachment();
@ -705,14 +729,12 @@ export class Spine extends Container {
texture = <SpineTexture>mesh.region?.texture; texture = <SpineTexture>mesh.region?.texture;
} else if (attachment instanceof ClippingAttachment) { } else if (attachment instanceof ClippingAttachment) {
Spine.clipper.clipStart(slot, attachment); Spine.clipper.clipStart(slot, attachment);
pixiMaskSource = { slot, computed: false };
continue; continue;
} else { } else {
if (this.hasMeshForSlot(slot)) { if (this.hasMeshForSlot(slot)) {
this.getMeshForSlot(slot).visible = false; this.getMeshForSlot(slot).visible = false;
} }
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
continue; continue;
} }
if (texture != null) { if (texture != null) {
@ -788,7 +810,6 @@ export class Spine extends Container {
} }
Spine.clipper.clipEndWithSlot(slot); Spine.clipper.clipEndWithSlot(slot);
this.pixiMaskCleanup(slot);
} }
Spine.clipper.clipEnd(); Spine.clipper.clipEnd();
} }
@ -997,10 +1018,14 @@ export class Spine extends Container {
} }
} }
type PixiMaskSource = { interface SlotsToClipping {
slot: Slot, slot: Slot,
computed: boolean, // prevent to reculaculate vertices for a mask clipping multiple pixi objects mask?: Graphics,
} maskComputed?: boolean,
vertices: Array<number>,
};
const maskPool = new Pool<Graphics>(() => new Graphics);
Skeleton.yDown = true; Skeleton.yDown = true;

View File

@ -568,7 +568,7 @@ export class Spine extends ViewContainer {
if (attachment && attachment instanceof ClippingAttachment) { if (attachment && attachment instanceof ClippingAttachment) {
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() }); const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() });
clip.maskComputed = false; clip.maskComputed = false;
this.currentClippingSlot = this.clippingSlotToPixiMasks[slot.data.name]; this.currentClippingSlot = clip;
return; return;
} }
@ -576,11 +576,8 @@ export class Spine extends ViewContainer {
let currentClippingSlot = this.currentClippingSlot; let currentClippingSlot = this.currentClippingSlot;
let slotObject = this._slotsObject[slot.data.name]; let slotObject = this._slotsObject[slot.data.name];
if (currentClippingSlot && slotObject) { if (currentClippingSlot && slotObject) {
let slotClipping = currentClippingSlot.slot;
let clippingAttachment = slotClipping.attachment as ClippingAttachment;
// create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot // create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot
let mask = currentClippingSlot.mask as Graphics; let mask = currentClippingSlot.mask;
if (!mask) { if (!mask) {
mask = maskPool.obtain(); mask = maskPool.obtain();
currentClippingSlot.mask = mask; currentClippingSlot.mask = mask;
@ -589,6 +586,8 @@ export class Spine extends ViewContainer {
// compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot // compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot
if (!currentClippingSlot.maskComputed) { if (!currentClippingSlot.maskComputed) {
let slotClipping = currentClippingSlot.slot;
let clippingAttachment = slotClipping.attachment as ClippingAttachment;
currentClippingSlot.maskComputed = true; currentClippingSlot.maskComputed = true;
const worldVerticesLength = clippingAttachment.worldVerticesLength; const worldVerticesLength = clippingAttachment.worldVerticesLength;
const vertices = currentClippingSlot.vertices; const vertices = currentClippingSlot.vertices;
@ -896,7 +895,6 @@ export class Spine extends ViewContainer {
container.includeInBuild = false; container.includeInBuild = false;
// TODO only add once??
this.addChild(container); this.addChild(container);
const slotObject = { const slotObject = {