[ts][canvaskit] Properly fix vertices info misalignment that led to crash when clipping (0b426772).

# Conflicts:
#	spine-ts/spine-canvaskit/src/index.ts
This commit is contained in:
Davide Tantillo 2025-07-22 16:10:52 +02:00
parent 1f813f8c29
commit 5826c4b1cf

View File

@ -1,3 +1,32 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. Replaces all prior versions.
*
* Copyright (c) 2013-2023, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
* otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
export * from "@esotericsoftware/spine-core"; export * from "@esotericsoftware/spine-core";
import { import {
@ -8,28 +37,20 @@ import {
ClippingAttachment, ClippingAttachment,
Color, Color,
MeshAttachment, MeshAttachment,
NumberArrayLike, type NumberArrayLike,
Physics, Physics,
RegionAttachment, RegionAttachment,
Skeleton, Skeleton,
SkeletonBinary, SkeletonBinary,
SkeletonClipping, SkeletonClipping,
SkeletonData, type SkeletonData,
SkeletonJson, SkeletonJson,
Texture, Texture,
TextureAtlas, TextureAtlas,
TextureFilter,
TextureWrap,
Utils, Utils,
} from "@esotericsoftware/spine-core"; } from "@esotericsoftware/spine-core";
import {
Canvas, import type { Canvas, CanvasKit, Image, Paint, Shader } from "canvaskit-wasm";
CanvasKit,
Image,
Paint,
Shader,
BlendMode as CanvasKitBlendMode,
} from "canvaskit-wasm";
Skeleton.yDown = true; Skeleton.yDown = true;
@ -55,7 +76,7 @@ function toCkBlendMode (ck: CanvasKit, blendMode: BlendMode) {
} }
} }
function bufferToUtf8String (buffer: any) { function bufferToUtf8String (buffer: ArrayBuffer | Buffer) {
if (typeof Buffer !== "undefined") { if (typeof Buffer !== "undefined") {
return buffer.toString("utf-8"); return buffer.toString("utf-8");
} else if (typeof TextDecoder !== "undefined") { } else if (typeof TextDecoder !== "undefined") {
@ -70,9 +91,9 @@ class CanvasKitTexture extends Texture {
return this._image; return this._image;
} }
setFilters (minFilter: TextureFilter, magFilter: TextureFilter): void { } setFilters (): void { }
setWraps (uWrap: TextureWrap, vWrap: TextureWrap): void { } setWraps (): void { }
dispose (): void { dispose (): void {
const data: CanvasKitImage = this._image; const data: CanvasKitImage = this._image;
@ -89,7 +110,7 @@ class CanvasKitTexture extends Texture {
static async fromFile ( static async fromFile (
ck: CanvasKit, ck: CanvasKit,
path: string, path: string,
readFile: (path: string) => Promise<any> readFile: (path: string) => Promise<ArrayBuffer | Buffer>
): Promise<CanvasKitTexture> { ): Promise<CanvasKitTexture> {
const imgData = await readFile(path); const imgData = await readFile(path);
if (!imgData) throw new Error(`Could not load image ${path}`); if (!imgData) throw new Error(`Could not load image ${path}`);
@ -126,7 +147,7 @@ class CanvasKitTexture extends Texture {
export async function loadTextureAtlas ( export async function loadTextureAtlas (
ck: CanvasKit, ck: CanvasKit,
atlasFile: string, atlasFile: string,
readFile: (path: string) => Promise<Buffer> readFile: (path: string) => Promise<ArrayBuffer | Buffer>
): Promise<TextureAtlas> { ): Promise<TextureAtlas> {
const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile))); const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile)));
const slashIndex = atlasFile.lastIndexOf("/"); const slashIndex = atlasFile.lastIndexOf("/");
@ -150,7 +171,7 @@ export async function loadTextureAtlas (
export async function loadSkeletonData ( export async function loadSkeletonData (
skeletonFile: string, skeletonFile: string,
atlas: TextureAtlas, atlas: TextureAtlas,
readFile: (path: string) => Promise<Buffer>, readFile: (path: string) => Promise<ArrayBuffer | Buffer>,
scale = 1 scale = 1
): Promise<SkeletonData> { ): Promise<SkeletonData> {
const attachmentLoader = new AtlasAttachmentLoader(atlas); const attachmentLoader = new AtlasAttachmentLoader(atlas);
@ -158,12 +179,11 @@ export async function loadSkeletonData (
? new SkeletonJson(attachmentLoader) ? new SkeletonJson(attachmentLoader)
: new SkeletonBinary(attachmentLoader); : new SkeletonBinary(attachmentLoader);
loader.scale = scale; loader.scale = scale;
let data = await readFile(skeletonFile); const data = await readFile(skeletonFile);
if (skeletonFile.endsWith(".json")) { if (loader instanceof SkeletonJson) {
data = bufferToUtf8String(data); return loader.readSkeletonData(bufferToUtf8String(data))
} }
const skeletonData = loader.readSkeletonData(data); return loader.readSkeletonData(data);
return skeletonData;
} }
/** /**
@ -224,12 +244,12 @@ export class SkeletonRenderer {
*/ */
render (canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) { render (canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) {
if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton; if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton;
let clipper = this.clipper; const clipper = this.clipper;
let drawOrder = skeleton.drawOrder; const drawOrder = skeleton.drawOrder;
let skeletonColor = skeleton.color; const skeletonColor = skeleton.color;
for (let i = 0, n = drawOrder.length; i < n; i++) { for (let i = 0, n = drawOrder.length; i < n; i++) {
let slot = drawOrder[i]; const slot = drawOrder[i];
if (!slot.bone.active) { if (!slot.bone.active) {
clipper.clipEnd(slot); clipper.clipEnd(slot);
continue; continue;
@ -245,20 +265,19 @@ export class SkeletonRenderer {
let attachmentColor: Color; let attachmentColor: Color;
let numVertices = 0; let numVertices = 0;
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
let region = attachment as RegionAttachment; const region = attachment;
positions = positions.length < 8 ? Utils.newFloatArray(8) : positions;
numVertices = 4; numVertices = 4;
region.computeWorldVertices(slot, positions, 0, 2); region.computeWorldVertices(slot, positions, 0, 2);
triangles = SkeletonRenderer.QUAD_TRIANGLES; triangles = SkeletonRenderer.QUAD_TRIANGLES;
uvs = region.uvs as Float32Array; uvs = region.uvs as Float32Array;
texture = region.region?.texture as CanvasKitTexture; texture = region.region ?.texture as CanvasKitTexture;
attachmentColor = region.color; attachmentColor = region.color;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
let mesh = attachment as MeshAttachment; const mesh = attachment as MeshAttachment;
positions = if (positions.length < mesh.worldVerticesLength) {
positions.length < mesh.worldVerticesLength this.scratchPositions = Utils.newFloatArray(mesh.worldVerticesLength);
? Utils.newFloatArray(mesh.worldVerticesLength) positions = this.scratchPositions;
: positions; }
numVertices = mesh.worldVerticesLength >> 1; numVertices = mesh.worldVerticesLength >> 1;
mesh.computeWorldVertices( mesh.computeWorldVertices(
skeleton, skeleton,
@ -270,7 +289,7 @@ export class SkeletonRenderer {
2 2
); );
triangles = mesh.triangles; triangles = mesh.triangles;
texture = mesh.region?.texture as CanvasKitTexture; texture = mesh.region ?.texture as CanvasKitTexture;
uvs = mesh.uvs as Float32Array; uvs = mesh.uvs as Float32Array;
attachmentColor = mesh.color; attachmentColor = mesh.color;
} else if (attachment instanceof ClippingAttachment) { } else if (attachment instanceof ClippingAttachment) {
@ -283,27 +302,47 @@ export class SkeletonRenderer {
} }
if (texture) { if (texture) {
let scaledUvs: NumberArrayLike;
if (clipper.isClipping()) { if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked( clipper.clipTrianglesUnpacked(positions, triangles, triangles.length, uvs);
positions, if (clipper.clippedVertices.length <= 0) {
triangles, clipper.clipEnd(slot);
triangles.length, continue;
uvs }
);
positions = clipper.clippedVertices; positions = clipper.clippedVertices;
uvs = clipper.clippedUVs; uvs = clipper.clippedUVs;
scaledUvs = clipper.clippedUVs;
triangles = clipper.clippedTriangles; triangles = clipper.clippedTriangles;
numVertices = clipper.clippedVertices.length / 2;
colors = Utils.newFloatArray(numVertices * 4);
} else {
scaledUvs = this.scratchUVs;
if (this.scratchUVs.length < uvs.length) {
this.scratchUVs = Utils.newFloatArray(uvs.length);
scaledUvs = this.scratchUVs;
}
if (colors.length / 4 < numVertices) {
this.scratchColors = Utils.newFloatArray(numVertices * 4);
colors = this.scratchColors;
}
} }
let slotColor = pose.color; const ckImage = texture.getImage();
let finalColor = this.tempColor; const image = ckImage.image;
const width = image.width();
const height = image.height();
for (let i = 0; i < uvs.length; i += 2) {
scaledUvs[i] = uvs[i] * width;
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const slotColor = pose.color;
const finalColor = this.tempColor;
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b;
finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a; finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a;
if (colors.length / 4 < numVertices)
colors = Utils.newFloatArray(numVertices * 4);
for (let i = 0, n = numVertices * 4; i < n; i += 4) { for (let i = 0, n = numVertices * 4; i < n; i += 4) {
colors[i] = finalColor.r; colors[i] = finalColor.r;
colors[i + 1] = finalColor.g; colors[i + 1] = finalColor.g;
@ -311,18 +350,6 @@ export class SkeletonRenderer {
colors[i + 3] = finalColor.a; colors[i + 3] = finalColor.a;
} }
const scaledUvs =
this.scratchUVs.length < uvs.length
? Utils.newFloatArray(uvs.length)
: this.scratchUVs;
const width = texture.getImage().image.width();
const height = texture.getImage().image.height();
for (let i = 0; i < uvs.length; i += 2) {
scaledUvs[i] = uvs[i] * width;
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const blendMode = slot.data.blendMode;
const vertices = this.ck.MakeVertices( const vertices = this.ck.MakeVertices(
this.ck.VertexMode.Triangles, this.ck.VertexMode.Triangles,
positions, positions,
@ -331,11 +358,8 @@ export class SkeletonRenderer {
triangles, triangles,
false false
); );
canvas.drawVertices( const ckPaint = ckImage.paintPerBlendMode.get(slot.data.blendMode);
vertices, if (ckPaint) canvas.drawVertices(vertices, this.ck.BlendMode.Modulate, ckPaint);
this.ck.BlendMode.Modulate,
texture.getImage().paintPerBlendMode.get(blendMode)!
);
vertices.delete(); vertices.delete();
} }