Changed Updated Bone Pose to Set Bone Pose with the ability of holding a pose, and modif rotation and scale too. Add Release Bone Hold.

This commit is contained in:
Davide Tantillo 2026-02-04 15:01:22 +01:00
parent f46c3fa67e
commit 3e15e3f688
6 changed files with 256 additions and 35 deletions

View File

@ -95,4 +95,15 @@ export class C3Matrix {
return this.skeletonToGame(applied.worldX, applied.worldY);
}
public gameToBoneRotation (gameAngleDeg: number, bone: Bone) {
return bone.applied.worldToLocalRotation(this.gameToSkeletonRotation(gameAngleDeg)) - 180;
}
public gameToSkeletonRotation (gameAngleDeg: number) {
const rad = gameAngleDeg * Math.PI / 180;
const sin = Math.sin(rad), cos = Math.cos(rad);
const { a, b, c, d } = this;
return Math.atan2(a * sin - b * cos, d * cos - c * sin) * (180 / Math.PI);
}
}

View File

@ -392,21 +392,63 @@
]
},
{
"id": "update-bone-pose",
"scriptName": "UpdateBonePose",
"id": "set-bone-pose",
"scriptName": "SetBonePose",
"highlight": false,
"params": [
{
"id": "bone-name",
"type": "string"
},
{
"id": "mode",
"type": "combo",
"items": ["local", "game"]
},
{
"id": "apply-mode",
"type": "combo",
"items": ["once", "hold"]
},
{
"id": "x",
"type": "number"
"type": "any",
"initialValue": ""
},
{
"id": "y",
"type": "number"
"type": "any",
"initialValue": ""
},
{
"id": "rotation",
"type": "any",
"initialValue": ""
},
{
"id": "scaleX",
"type": "any",
"initialValue": ""
},
{
"id": "scaleY",
"type": "any",
"initialValue": ""
}
]
},
{
"id": "release-bone-hold",
"scriptName": "ReleaseBoneHold",
"highlight": false,
"params": [
{
"id": "bone-name",
"type": "string"
},
{
"id": "reset-to-setup",
"type": "boolean"
}
]
},

View File

@ -121,8 +121,21 @@ C3.Plugins.EsotericSoftware_SpineConstruct3.Acts =
this.resetSlotColors(slotName);
},
UpdateBonePose (this: SDKInstanceClass, x: number, y: number, boneName: string) {
this.updateBonePose(x, y, boneName);
SetBonePose (this: SDKInstanceClass, boneName: string, mode: 0 | 1, applyMode: 0 | 1, x: number | string, y: number | string, rotation: number | string, scaleX: number | string, scaleY: number | string) {
this.setBonePose(
boneName,
mode === 0 ? "local" : "game",
applyMode === 0 ? "once" : "hold",
toNumberOrUndefined(x),
toNumberOrUndefined(y),
toNumberOrUndefined(rotation),
toNumberOrUndefined(scaleX),
toNumberOrUndefined(scaleY),
);
},
ReleaseBoneHold (this: SDKInstanceClass, boneName: string, resetToSetup: boolean) {
this.releaseBoneHold(boneName, resetToSetup);
},
AttachInstanceToBone (this: SDKInstanceClass, uid: number, boneName: string, offsetX: number, offsetY: number, offsetAngle: number) {
@ -142,3 +155,19 @@ C3.Plugins.EsotericSoftware_SpineConstruct3.Acts =
}
};
function toNumberOrUndefined (x: number | string): number | undefined {
if (typeof x === "number") {
return Number.isFinite(x) ? x : undefined;
}
if (typeof x === "string") {
const trimmed = x.trim();
if (trimmed === "") return undefined;
const n = Number(trimmed);
return Number.isFinite(n) ? n : undefined;
}
return undefined;
}

View File

@ -27,13 +27,15 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import type { AnimationState, AssetLoader, Bone, C3Matrix, C3RendererRuntime, Event, NumberArrayLike, Skeleton, Skin, Slot, SpineBoundsProvider, SpineBoundsProviderType, TextureAtlas, } from "@esotericsoftware/spine-construct3-lib";
import type { AnimationState, AssetLoader, Bone, BoneLocal, C3Matrix, C3RendererRuntime, Event, NumberArrayLike, Skeleton, Skin, Slot, SpineBoundsProvider, SpineBoundsProviderType, TextureAtlas, } from "@esotericsoftware/spine-construct3-lib";
const C3 = globalThis.C3;
const spine = globalThis.spine;
spine.Skeleton.yDown = true;
type BoneOverride = Partial<BoneLocal> & { mode: "game" | "local" };
class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
propAtlas = "";
propSkel = "";
@ -87,6 +89,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
private boneFollowers = new Map<string, { uid: number, offsetX: number, offsetY: number, offsetAngle: number }>();
private bonesOverride: Map<Bone, BoneOverride> = new Map();
private dragHandles = new Set<{
slot?: Slot,
bone: Bone,
@ -180,6 +184,8 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
this.updateHandles(skeleton, matrix);
this.updateBonesOverride();
skeleton.updateWorldTransform(physicsMode);
this.updateBoneFollowers(matrix);
@ -846,6 +852,37 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
}
}
private updateBonesOverride () {
for (const [bone, override] of this.bonesOverride) {
this.updateBonePoseOnce(bone, override);
}
}
private updateBonePoseOnce (bone: Bone, boneOverride: BoneOverride) {
const { mode, x, y, rotation, scaleX, scaleY } = boneOverride;
if (mode === "game") {
if (x !== undefined || y !== undefined) {
const locals = this.matrix.gameToBone(
x ?? this.matrix.boneToGame(bone).x,
y ?? this.matrix.boneToGame(bone).y,
bone);
bone.pose.x = locals.x;
bone.pose.y = locals.y;
}
if (rotation !== undefined) bone.pose.rotation = this.matrix.gameToBoneRotation(rotation, bone);
}
if (mode === "local") {
if (x !== undefined) bone.pose.x = x;
if (y !== undefined) bone.pose.y = y;
if (rotation !== undefined) bone.pose.rotation = rotation;
}
if (scaleX !== undefined) bone.pose.scaleX = scaleX;
if (scaleY !== undefined) bone.pose.scaleY = scaleY;
}
/**********/
/*
@ -957,13 +994,29 @@ class SpineC3Instance extends globalThis.ISDKWorldInstanceBase {
return point.y;
}
public updateBonePose (c3X: number, c3Y: number, boneName: string) {
public setBonePose (boneName: string, mode: "game" | "local", applyMode: "once" | "hold", c3X?: number, c3Y?: number, c3Rotation?: number, scaleX?: number, scaleY?: number) {
const bone = this.getBone(boneName);
if (!bone) return;
if (applyMode === "hold") {
const existing = this.bonesOverride.get(bone);
this.bonesOverride.set(bone, {
mode,
x: c3X ?? existing?.x,
y: c3Y ?? existing?.y,
rotation: c3Rotation ?? existing?.rotation,
scaleX: scaleX ?? existing?.scaleX,
scaleY: scaleY ?? existing?.scaleY,
});
} else {
this.updateBonePoseOnce(bone, { mode, x: c3X, y: c3Y, rotation: c3Rotation, scaleX, scaleY });
}
}
const { x, y } = this.matrix.gameToBone(c3X, c3Y, bone);
bone.applied.x = x;
bone.applied.y = y;
public releaseBoneHold (boneName: string, resetToSetup: boolean) {
const bone = this.getBone(boneName);
if (!bone) return;
this.bonesOverride.delete(bone);
if (resetToSetup) bone.setupPose();
}
private getBone (boneName: string | Bone) {

View File

@ -483,22 +483,65 @@
}
}
},
"update-bone-pose": {
"list-name": "Update bone pose",
"display-text": "Set bone {2} position to ({0}, {1})",
"description": "Update the pose position of a bone to specific world coordinates.",
"set-bone-pose": {
"list-name": "Set bone pose",
"display-text": "Set bone {0} position ({3}, {4}), rotation {5}, scale ({6}, {7}) in {1} mode, {2}",
"description": "Set a bone's pose. Once: applied for the current frame only. Hold: re-applied every frame, merging with previously held values. Use Release bone hold to stop holding. Position and rotation use the selected coordinate space (game or local). Scale is always local.",
"params": {
"x": {
"name": "X",
"desc": "X world coordinate"
},
"y": {
"name": "Y",
"desc": "Y world coordinate"
},
"bone-name": {
"name": "Bone name",
"desc": "Name of the bone to update"
},
"mode": {
"name": "Mode",
"desc": "Coordinate space for position and rotation. Local: values are applied directly as the bone's local pose. Game: values are converted from game space.",
"items": {
"local": "Local",
"game": "Game"
}
},
"apply-mode": {
"name": "Apply",
"desc": "Once: the pose is applied for the current frame only. Hold: the pose is re-applied every frame until released. In hold mode, values left as empty string preserve the previously held value on that axis.",
"items": {
"once": "Once",
"hold": "Hold"
}
},
"x": {
"name": "X",
"desc": "X position. Game space in game mode, bone-local in local mode. Leave as empty string to keep current value."
},
"y": {
"name": "Y",
"desc": "Y position. Game space in game mode, bone-local in local mode. Leave as empty string to keep current value."
},
"rotation": {
"name": "Rotation",
"desc": "Rotation in degrees. Game space in game mode, bone-local in local mode. Leave as empty string to keep current value."
},
"scaleX": {
"name": "Scale X",
"desc": "Bone's local scale X. Always applied as local scale regardless of mode. Leave as empty string to keep current value."
},
"scaleY": {
"name": "Scale Y",
"desc": "Bone's local scale Y. Always applied as local scale regardless of mode. Leave as empty string to keep current value."
}
}
},
"release-bone-hold": {
"list-name": "Release bone hold",
"display-text": "Release hold on bone {0}, reset to setup pose: {1}",
"description": "Release a held bone pose, allowing the animation to control the bone again. Optionally resets the bone to its setup pose.",
"params": {
"bone-name": {
"name": "Bone name",
"desc": "Name of the bone to release"
},
"reset-to-setup": {
"name": "Reset to setup pose",
"desc": "If checked, resets the bone to its setup pose after releasing the hold."
}
}
},

View File

@ -483,22 +483,65 @@
}
}
},
"update-bone-pose": {
"list-name": "更新骨骼姿势",
"display-text": "将骨骼{2}的位置设置为({0}, {1})",
"description": "将骨骼的姿势位置更新为特定的世界坐标。",
"set-bone-pose": {
"list-name": "设置骨骼姿势",
"display-text": "设置骨骼{0}位置({3}, {4}),旋转{5},缩放({6}, {7}){1}模式,{2}",
"description": "设置骨骼姿势。一次:仅在当前帧应用。保持:每帧重新应用,与之前保持的值合并。使用释放骨骼保持操作停止保持。位置和旋转使用所选的坐标空间(游戏或本地)。缩放始终为本地值。",
"params": {
"x": {
"name": "X",
"desc": "X世界坐标"
},
"y": {
"name": "Y",
"desc": "Y世界坐标"
},
"bone-name": {
"name": "骨骼名称",
"desc": "要更新的骨骼名称"
},
"mode": {
"name": "模式",
"desc": "位置和旋转的坐标空间。本地:值直接作为骨骼本地姿势应用。游戏:值从游戏空间转换。",
"items": {
"local": "本地",
"game": "游戏"
}
},
"apply-mode": {
"name": "应用方式",
"desc": "一次:仅在当前帧应用姿势。保持:每帧重新应用姿势,直到释放。保持模式下,留为空字符串的值将保留该轴之前的保持值。",
"items": {
"once": "一次",
"hold": "保持"
}
},
"x": {
"name": "X",
"desc": "X位置。游戏模式下为游戏空间坐标本地模式下为骨骼本地坐标。设为空字符串保持当前值。"
},
"y": {
"name": "Y",
"desc": "Y位置。游戏模式下为游戏空间坐标本地模式下为骨骼本地坐标。设为空字符串保持当前值。"
},
"rotation": {
"name": "旋转",
"desc": "旋转角度(度)。游戏模式下为游戏空间角度,本地模式下为骨骼本地角度。设为空字符串保持当前值。"
},
"scaleX": {
"name": "缩放X",
"desc": "骨骼的本地缩放X。无论模式如何始终作为本地缩放应用。设为空字符串保持当前值。"
},
"scaleY": {
"name": "缩放Y",
"desc": "骨骼的本地缩放Y。无论模式如何始终作为本地缩放应用。设为空字符串保持当前值。"
}
}
},
"release-bone-hold": {
"list-name": "释放骨骼保持",
"display-text": "释放骨骼{0}的保持,重置为初始姿势:{1}",
"description": "释放已保持的骨骼姿势,允许动画重新控制该骨骼。可选择将骨骼重置为初始姿势。",
"params": {
"bone-name": {
"name": "骨骼名称",
"desc": "要释放的骨骼名称"
},
"reset-to-setup": {
"name": "重置为初始姿势",
"desc": "如果勾选,释放保持后将骨骼重置为初始姿势。"
}
}
},