mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-20 17:26:01 +08:00
490 lines
14 KiB
TypeScript
490 lines
14 KiB
TypeScript
// / <reference types="editor/sdk" />
|
|
|
|
import type { AnimationState, AssetLoader, Skeleton, SkeletonRendererCore, SpineBoundsProvider, TextureAtlas } from "@esotericsoftware/spine-construct3-lib";
|
|
import type { SpineC3PluginType } from "./type";
|
|
|
|
const SDK = globalThis.SDK;
|
|
|
|
const PLUGIN_CLASS = SDK.Plugins.EsotericSoftware_SpineConstruct3;
|
|
|
|
let spine: typeof globalThis.spine;
|
|
|
|
type SpineBoundsProviderType = "setup" | "animation-skin" | "AABB";
|
|
|
|
class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
|
|
private layoutView?: SDK.UI.ILayoutView;
|
|
private renderer?: SDK.Gfx.IWebGLRenderer;
|
|
|
|
private currentAtlasFileSID = -1;
|
|
private textureAtlas?: TextureAtlas;
|
|
|
|
skeleton?: Skeleton;
|
|
state?: AnimationState;
|
|
skins: string[] = [];
|
|
animation?: string;
|
|
|
|
_inst!: SDK.IWorldInstance & { errors: SpineC3EditorError };
|
|
|
|
private assetLoader: AssetLoader;
|
|
private skeletonRenderer: SkeletonRendererCore;
|
|
|
|
// position mode
|
|
private positioningBounds = false;
|
|
private positionModePrevX = 0;
|
|
private positionModePrevY = 0;
|
|
private positionModePrevAngle = 0;
|
|
private spineBounds = {
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
};
|
|
|
|
// utils for drawing
|
|
private tempVertices = new Float32Array(4096);
|
|
private tempColors = new Float32Array(4096);
|
|
|
|
// errors
|
|
private errors: SpineC3EditorError = {};
|
|
|
|
constructor (sdkType: SDK.ITypeBase, inst: SDK.IWorldInstance) {
|
|
super(sdkType, inst);
|
|
|
|
if (!spine) spine = globalThis.spine;
|
|
spine.Skeleton.yDown = true;
|
|
|
|
this.assetLoader = new spine.AssetLoader();
|
|
this.skeletonRenderer = new spine.SkeletonRendererCore();
|
|
|
|
this._inst.errors = this.errors;
|
|
}
|
|
|
|
Release () {
|
|
}
|
|
|
|
OnCreate () {
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_PROVIDER_MOVE, false);
|
|
}
|
|
|
|
OnPlacedInLayout () {
|
|
this.OnMakeOriginalSize();
|
|
}
|
|
|
|
Draw (iRenderer: SDK.Gfx.IWebGLRenderer, iDrawParams: SDK.Gfx.IDrawParams) {
|
|
this.layoutView ||= iDrawParams.GetLayoutView();
|
|
this.renderer ||= iRenderer;
|
|
|
|
this.loadAtlas();
|
|
this.loadSkeleton();
|
|
|
|
const hasErrors = this.hasErrors();
|
|
if (this.skeleton && !hasErrors) {
|
|
this.setAnimation();
|
|
this.setSkin();
|
|
|
|
const rectX = this._inst.GetX();
|
|
const rectY = this._inst.GetY();
|
|
const rectAngle = this._inst.GetAngle();
|
|
let offsetX = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_X) as number;
|
|
let offsetY = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_Y) as number;
|
|
let offsetAngle = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_ANGLE) as number;
|
|
|
|
if (!this.positioningBounds) {
|
|
offsetX += rectX;
|
|
offsetY += rectY;
|
|
offsetAngle += rectAngle;
|
|
|
|
const baseScaleX = this._inst.GetWidth() / this.spineBounds.width;
|
|
const baseScaleY = this._inst.GetHeight() / this.spineBounds.height;
|
|
this.skeleton.scaleX = baseScaleX;
|
|
this.skeleton.scaleY = baseScaleY;
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_X, baseScaleX);
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_Y, baseScaleY);
|
|
} else {
|
|
offsetX += this.positionModePrevX;
|
|
offsetY += this.positionModePrevY;
|
|
offsetAngle += this.positionModePrevAngle;
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_X, offsetX - rectX);
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_Y, offsetY - rectY);
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_ANGLE, offsetAngle - rectAngle);
|
|
this.positionModePrevX = rectX;
|
|
this.positionModePrevY = rectY;
|
|
this.positionModePrevAngle = rectAngle;
|
|
|
|
this.skeleton.scaleX = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_X) as number;
|
|
this.skeleton.scaleY = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_Y) as number;
|
|
}
|
|
|
|
|
|
const cos = Math.cos(offsetAngle);
|
|
const sin = Math.sin(offsetAngle);
|
|
const inv255 = 1 / 255;
|
|
|
|
this.update(0);
|
|
let command = this.skeletonRenderer.render(this.skeleton);
|
|
while (command) {
|
|
const { numVertices, positions, uvs, colors, indices, numIndices, blendMode } = command;
|
|
|
|
const vertices = this.tempVertices;
|
|
const c3colors = this.tempColors;
|
|
for (let i = 0; i < numVertices; i++) {
|
|
const srcIndex = i * 2;
|
|
const dstIndex = i * 3;
|
|
const x = positions[srcIndex];
|
|
const y = positions[srcIndex + 1];
|
|
vertices[dstIndex] = x * cos - y * sin + offsetX;
|
|
vertices[dstIndex + 1] = x * sin + y * cos + offsetY;
|
|
vertices[dstIndex + 2] = 0;
|
|
|
|
const color = colors[i];
|
|
const colorDst = i * 4;
|
|
c3colors[colorDst] = (color >>> 16 & 0xFF) * inv255;
|
|
c3colors[colorDst + 1] = (color >>> 8 & 0xFF) * inv255;
|
|
c3colors[colorDst + 2] = (color & 0xFF) * inv255;
|
|
c3colors[colorDst + 3] = (color >>> 24 & 0xFF) * inv255;
|
|
}
|
|
|
|
iRenderer.ResetColor();
|
|
iRenderer.SetBlendMode(spine.BlendingModeSpineToC3[blendMode]);
|
|
iRenderer.SetTextureFillMode();
|
|
iRenderer.SetTexture(command.texture.texture);
|
|
|
|
iRenderer.DrawMesh(
|
|
vertices.subarray(0, numVertices * 3),
|
|
uvs.subarray(0, numVertices * 2),
|
|
indices.subarray(0, numIndices),
|
|
c3colors,
|
|
);
|
|
|
|
|
|
command = command.next;
|
|
}
|
|
|
|
iRenderer.SetAlphaBlend();
|
|
iRenderer.SetColorFillMode();
|
|
iRenderer.SetColorRgba(0.25, 0, 0, 0.25);
|
|
iRenderer.LineQuad(this._inst.GetQuad());
|
|
iRenderer.Line(rectX, rectY, offsetX, offsetY);
|
|
|
|
// if (this.hasErrors()) {
|
|
// iRenderer.SetColorFillMode();
|
|
// iRenderer.SetColorRgba(1, 0, 0, .5);
|
|
// iRenderer.Quad(this._inst.GetQuad());
|
|
// }
|
|
} else {
|
|
|
|
const sdkType = this._sdkType as SpineC3PluginType;
|
|
|
|
const logo = sdkType.getSpineLogo(iRenderer);
|
|
if (logo) {
|
|
iRenderer.ResetColor();
|
|
iRenderer.SetAlphaBlend();
|
|
iRenderer.SetTexture(logo);
|
|
if (hasErrors) {
|
|
iRenderer.SetColorRgba(1, 0, 0, 1);
|
|
}
|
|
iRenderer.Quad(this._inst.GetQuad());
|
|
} else {
|
|
iRenderer.SetAlphaBlend();
|
|
iRenderer.SetColorFillMode();
|
|
|
|
if (this.HadTextureError())
|
|
iRenderer.SetColorRgba(0.25, 0, 0, 0.25);
|
|
else
|
|
iRenderer.SetColorRgba(0, 0, 0.1, 0.1);
|
|
|
|
iRenderer.Quad(this._inst.GetQuad());
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
async OnPropertyChanged (id: string, value: EditorPropertyValueType) {
|
|
console.log(`Prop change - Name: ${id} - Value: ${value}`);
|
|
|
|
if (id === PLUGIN_CLASS.PROP_ATLAS) {
|
|
this.textureAtlas?.dispose();
|
|
this.textureAtlas = undefined;
|
|
this.skins = [];
|
|
this.layoutView?.Refresh();
|
|
return;
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_SKELETON) {
|
|
this.skeleton = undefined;
|
|
this.skins = [];
|
|
this.layoutView?.Refresh();
|
|
return;
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_LOADER_SCALE) {
|
|
this.skeleton = undefined;
|
|
this.skins = [];
|
|
this.layoutView?.Refresh();
|
|
return;
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_SKIN) {
|
|
this.skins = [];
|
|
this.setSkin();
|
|
this.resetBounds();
|
|
this.layoutView?.Refresh();
|
|
return;
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_ANIMATION) {
|
|
this.setAnimation();
|
|
this.layoutView?.Refresh();
|
|
return;
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_BOUNDS_PROVIDER) {
|
|
this.resetBounds();
|
|
this.layoutView?.Refresh();
|
|
return
|
|
}
|
|
|
|
if (id === PLUGIN_CLASS.PROP_BOUNDS_PROVIDER_MOVE) {
|
|
value = value as boolean
|
|
if (value) {
|
|
this.positionModePrevX = this._inst.GetX();
|
|
this.positionModePrevY = this._inst.GetY();
|
|
this.positionModePrevAngle = this._inst.GetAngle();
|
|
} else {
|
|
const scaleX = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_X) as number;
|
|
const scaleY = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_Y) as number;
|
|
this.spineBounds.width = this._inst.GetWidth() / scaleX;
|
|
this.spineBounds.height = this._inst.GetHeight() / scaleY;
|
|
}
|
|
this.positioningBounds = value;
|
|
return
|
|
}
|
|
|
|
console.log("Prop change end");
|
|
}
|
|
|
|
private setAnimation () {
|
|
this.animation = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION) as string;
|
|
}
|
|
|
|
private setSkin () {
|
|
const { skeleton } = this;
|
|
if (!skeleton) return;
|
|
|
|
const propValue = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKIN) as string;
|
|
|
|
const skins = propValue === "" ? [] : propValue.split(",");
|
|
this.skins = skins;
|
|
|
|
if (skins.length === 0) {
|
|
skeleton.setSkin(null);
|
|
} else if (skins.length === 1) {
|
|
const skinName = skins[0];
|
|
const skin = skeleton.data.findSkin(skinName);
|
|
if (!skin) {
|
|
// TODO: signal error
|
|
return;
|
|
}
|
|
skeleton.setSkin(skins[0]);
|
|
} else {
|
|
const customSkin = new spine.Skin(propValue);
|
|
for (const s of skins) {
|
|
const skin = skeleton.data.findSkin(s)
|
|
if (!skin) {
|
|
// TODO: signal error
|
|
return;
|
|
}
|
|
customSkin.addSkin(skin);
|
|
}
|
|
skeleton.setSkin(customSkin);
|
|
}
|
|
|
|
skeleton.setupPose();
|
|
this.update(0);
|
|
}
|
|
|
|
private async loadSkeleton () {
|
|
if (!this.renderer || !this.textureAtlas) return;
|
|
if (this.skeleton) return;
|
|
|
|
console.log("Loading skeleton");
|
|
|
|
const propValue = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON) as number;
|
|
const loaderScale = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_LOADER_SCALE) as number;
|
|
const skeletonData = await this.assetLoader.loadSkeletonEditor(propValue, this.textureAtlas, loaderScale, this._inst)
|
|
.catch((error) => {
|
|
console.log("ATLAS AND SKELETON NOT CORRESPONDING", error);
|
|
});
|
|
if (!skeletonData) return;
|
|
|
|
this.skeleton = new spine.Skeleton(skeletonData);
|
|
const animationStateData = new spine.AnimationStateData(skeletonData);
|
|
this.state = new spine.AnimationState(animationStateData);
|
|
|
|
this.setSkin();
|
|
this.update(0);
|
|
this.setBoundsFromBoundsProvider();
|
|
this.initBounds();
|
|
|
|
this.layoutView?.Refresh();
|
|
console.log("SKELETON LOADED");
|
|
}
|
|
|
|
private async loadAtlas () {
|
|
if (!this.renderer) return;
|
|
|
|
const propValue = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ATLAS) as number;
|
|
|
|
if (this.currentAtlasFileSID === propValue) return;
|
|
this.currentAtlasFileSID = propValue;
|
|
|
|
console.log("Loading atlas");
|
|
|
|
const textureAtlas = await this.assetLoader.loadAtlasEditor(propValue, this._inst, this.renderer);
|
|
if (!textureAtlas) return;
|
|
|
|
this.textureAtlas = textureAtlas;
|
|
this.layoutView?.Refresh();
|
|
}
|
|
|
|
private setBoundsFromBoundsProvider () {
|
|
const propValue = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_PROVIDER) as SpineBoundsProviderType;
|
|
|
|
let spineBoundsProvider: SpineBoundsProvider;
|
|
if (propValue === "animation-skin") {
|
|
const { skins, animation } = this;
|
|
if ((skins && skins.length > 0) || animation) {
|
|
spineBoundsProvider = new spine.SkinsAndAnimationBoundsProvider(animation, skins);
|
|
} else {
|
|
return false;
|
|
}
|
|
} else if (propValue === "setup") {
|
|
spineBoundsProvider = new spine.SetupPoseBoundsProvider();
|
|
} else {
|
|
spineBoundsProvider = new spine.AABBRectangleBoundsProvider(0, 0, 100, 100);
|
|
}
|
|
|
|
this.spineBounds = spineBoundsProvider.calculateBounds(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
private resetBounds () {
|
|
this.setBoundsFromBoundsProvider();
|
|
|
|
if (this.hasErrors()) return;
|
|
const { x, y, width, height } = this.spineBounds;
|
|
|
|
this._inst.SetOrigin(-x / width, -y / height);
|
|
this._inst.SetSize(width, height);
|
|
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_X, 0);
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_Y, 0);
|
|
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_ANGLE, 0);
|
|
return;
|
|
}
|
|
|
|
private initBounds () {
|
|
const offsetX = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_X) as number;
|
|
const offsetY = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_Y) as number;
|
|
const offsetAngle = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_OFFSET_ANGLE) as number;
|
|
const shiftedBounds = offsetX !== 0 || offsetY !== 0 || offsetAngle !== 0;
|
|
|
|
const scaleX = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_X) as number;
|
|
const scaleY = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKELETON_SCALE_Y) as number;
|
|
const scaledBounds = scaleX !== 1 || scaleY !== 1;
|
|
|
|
if (shiftedBounds || scaledBounds) {
|
|
this.spineBounds.width = this._inst.GetWidth() / scaleX;
|
|
this.spineBounds.height = this._inst.GetHeight() / scaleY;
|
|
return;
|
|
}
|
|
|
|
this.resetBounds();
|
|
}
|
|
|
|
private update (delta: number) {
|
|
const { state, skeleton } = this;
|
|
|
|
if (!skeleton || !state) return;
|
|
|
|
state.update(delta);
|
|
skeleton.update(delta);
|
|
state.apply(skeleton);
|
|
skeleton.updateWorldTransform(spine.Physics.update);
|
|
}
|
|
|
|
private setError (key: SpineC3EditorErrorType, condition: boolean, message: string) {
|
|
if (condition) {
|
|
this.errors[key] = message;
|
|
return;
|
|
}
|
|
delete this.errors[key];
|
|
}
|
|
|
|
private hasErrors () {
|
|
const { errors, skins, animation, spineBounds } = this;
|
|
|
|
const boundsType = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_BOUNDS_PROVIDER) as SpineBoundsProviderType;
|
|
this.setError(
|
|
"boundsAnimationSkinType",
|
|
boundsType === "animation-skin" && ((!skins || skins.length === 0) && !animation),
|
|
"Animation/Skin bounds provider requires one between skin and animation to be set."
|
|
);
|
|
|
|
this.setError(
|
|
"nonExistingAnimation",
|
|
Boolean(!animation || this.skeleton?.data.findAnimation(animation)) === false,
|
|
"Not existing animation"
|
|
);
|
|
|
|
const { width, height } = spineBounds;
|
|
this.setError(
|
|
"boundsNoDimension",
|
|
width <= 0 || height <= 0,
|
|
"A bounds cannot have negative dimensions. This might happen when the setup pose is empty. Try to set a skin and the Animation/Skin bounds provider."
|
|
);
|
|
|
|
return Object.keys(errors).length > 0;
|
|
}
|
|
|
|
GetTexture () {
|
|
const image = this.GetObjectType().GetImage();
|
|
return super.GetTexture(image);
|
|
}
|
|
|
|
IsOriginalSizeKnown () {
|
|
return true;
|
|
}
|
|
|
|
GetOriginalWidth () {
|
|
return this.spineBounds.width;
|
|
}
|
|
|
|
GetOriginalHeight () {
|
|
return this.spineBounds.height;
|
|
}
|
|
|
|
OnMakeOriginalSize () {
|
|
this._inst.SetSize(this.spineBounds.width, this.spineBounds.height);
|
|
}
|
|
|
|
HasDoubleTapHandler () {
|
|
return false;
|
|
}
|
|
|
|
OnDoubleTap () { }
|
|
|
|
LoadC2Property (_name: string, _valueString: string) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
type SpineC3EditorErrorType = "boundsAnimationSkinType" | "nonExistingAnimation" | "boundsNoDimension";
|
|
type SpineC3EditorError = Partial<Record<SpineC3EditorErrorType, string>>;
|
|
|
|
PLUGIN_CLASS.Instance = SpineC3PluginInstance;
|
|
|
|
export type { SpineC3PluginInstance as SDKEditorInstanceClass };
|