@energy8platform/golem 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/editor.js +69 -14
- package/dist/lib/cli.js +209 -64
- package/dist/lib/cli.js.map +3 -3
- package/dist/lib/editor/store.d.ts +105 -0
- package/dist/lib/editor-entry.js +209 -64
- package/dist/lib/editor-entry.js.map +3 -3
- package/dist/lib/harness.js +69 -14
- package/dist/lib/interactive-editor.js.map +1 -1
- package/dist/lib/rig-anim.d.ts +2 -0
- package/dist/lib/rig-control-qa.d.ts +27 -0
- package/dist/lib/rig-format.d.ts +989 -0
- package/dist/lib/runtime.js +69 -14
- package/dist/lib/runtime.js.map +3 -3
- package/dist/lib/spine-import.d.ts +4 -0
- package/dist/lib/tools.js +209 -64
- package/dist/lib/tools.js.map +3 -3
- package/package.json +1 -1
|
@@ -26,6 +26,8 @@ export declare function parseAtlas(text: string): {
|
|
|
26
26
|
};
|
|
27
27
|
export interface ImportOptions {
|
|
28
28
|
name?: string;
|
|
29
|
+
/** Flatten this skin into setup/attachment timelines, falling back only to the default skin. */
|
|
30
|
+
skin?: string;
|
|
29
31
|
/** file name to use for the atlas page image(s) in asset.src (e.g. "ross.png" after transcoding a ktx2) */
|
|
30
32
|
pageImage?: string | ((page: string) => string);
|
|
31
33
|
/** canvas margin around the skeleton bounds, px */
|
|
@@ -58,11 +60,13 @@ export interface SpineProjectOptions {
|
|
|
58
60
|
/** where the rig will live: the page asset is referenced relative to its directory (KTX2 pages are written there as PNG) */
|
|
59
61
|
rigPath: string;
|
|
60
62
|
name?: string;
|
|
63
|
+
skin?: string;
|
|
61
64
|
margin?: number;
|
|
62
65
|
notes?: string;
|
|
63
66
|
}
|
|
64
67
|
/** Import a Spine project from files. Resolves the atlas and page image, transcodes KTX2 pages to PNG next to the rig. */
|
|
65
68
|
export declare function importSpineProject(opts: SpineProjectOptions): Promise<ReturnType<typeof importSpine> & {
|
|
66
69
|
imagePath: string;
|
|
70
|
+
imagePaths: string[];
|
|
67
71
|
}>;
|
|
68
72
|
export {};
|
package/dist/lib/tools.js
CHANGED
|
@@ -216,7 +216,9 @@ var PathAttachment = z.object({
|
|
|
216
216
|
/** curve lengths for constant-speed positioning */
|
|
217
217
|
lengths: z.array(z.number()).default([]),
|
|
218
218
|
closed: z.boolean().default(false),
|
|
219
|
-
constantSpeed: z.boolean().default(true)
|
|
219
|
+
constantSpeed: z.boolean().default(true),
|
|
220
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
221
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
220
222
|
});
|
|
221
223
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
222
224
|
var IkConstraint = z.object({
|
|
@@ -285,7 +287,9 @@ var Key = z.object({
|
|
|
285
287
|
t: z.number().nonnegative(),
|
|
286
288
|
v: KeyValue,
|
|
287
289
|
/** easing INTO the next key */
|
|
288
|
-
ease: Ease.default("ease_in_out")
|
|
290
|
+
ease: Ease.default("ease_in_out"),
|
|
291
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
292
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
289
293
|
});
|
|
290
294
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
291
295
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -472,6 +476,10 @@ function validate(doc) {
|
|
|
472
476
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
473
477
|
}
|
|
474
478
|
}
|
|
479
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
480
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
481
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
482
|
+
}
|
|
475
483
|
const vertexCount2 = (a) => {
|
|
476
484
|
if (a.type === "region") return 4;
|
|
477
485
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -555,6 +563,7 @@ function validate(doc) {
|
|
|
555
563
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
556
564
|
let prev = -1;
|
|
557
565
|
for (const k of t.keys) {
|
|
566
|
+
if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
|
|
558
567
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
559
568
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
560
569
|
if (t.target === "control") {
|
|
@@ -732,8 +741,8 @@ function sample(track, t, loop = false) {
|
|
|
732
741
|
}
|
|
733
742
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
734
743
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
735
|
-
const
|
|
736
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
744
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
745
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
737
746
|
}
|
|
738
747
|
return a.v;
|
|
739
748
|
}
|
|
@@ -825,6 +834,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
825
834
|
if (!parent) return local;
|
|
826
835
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
827
836
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
837
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
838
|
+
const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
|
|
839
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
840
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
841
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
842
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
843
|
+
}
|
|
828
844
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
829
845
|
}
|
|
830
846
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -857,11 +873,11 @@ var localOf = (doc, pose, b) => {
|
|
|
857
873
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
858
874
|
};
|
|
859
875
|
};
|
|
860
|
-
function updateSubtree(skel, root2) {
|
|
876
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
861
877
|
const doc = skel.doc;
|
|
862
878
|
const kids = /* @__PURE__ */ new Map();
|
|
863
879
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
864
|
-
const stack = [root2];
|
|
880
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
865
881
|
while (stack.length) {
|
|
866
882
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
867
883
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1104,7 +1120,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1104
1120
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1105
1121
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1106
1122
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1107
|
-
const
|
|
1123
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1124
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1108
1125
|
for (const id of c.bones) {
|
|
1109
1126
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1110
1127
|
if (c.local) {
|
|
@@ -1266,6 +1283,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1266
1283
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1267
1284
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1268
1285
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1286
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1269
1287
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1270
1288
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1271
1289
|
let sum = 0;
|
|
@@ -1300,9 +1318,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1300
1318
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1301
1319
|
}
|
|
1302
1320
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1321
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1303
1322
|
for (let i = 0; i < bones.length; i++) {
|
|
1304
1323
|
const { b, st } = bones[i];
|
|
1305
|
-
let w = { ...
|
|
1324
|
+
let w = { ...inputWorlds[i] };
|
|
1306
1325
|
w.tx += (boneX - w.tx) * mixX;
|
|
1307
1326
|
w.ty += (boneY - w.ty) * mixY;
|
|
1308
1327
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1332,7 +1351,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1332
1351
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1333
1352
|
w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
|
|
1334
1353
|
}
|
|
1335
|
-
|
|
1354
|
+
solvedWorlds.set(b.id, w);
|
|
1355
|
+
}
|
|
1356
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1357
|
+
const world = solvedWorlds.get(b.id);
|
|
1358
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1336
1359
|
}
|
|
1337
1360
|
}
|
|
1338
1361
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1361,9 +1384,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1361
1384
|
case "onlyTranslation":
|
|
1362
1385
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1363
1386
|
case "noRotationOrReflection": {
|
|
1364
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1387
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1365
1388
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1366
|
-
|
|
1389
|
+
if (lenX * lenX <= 1e-4) {
|
|
1390
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1391
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1392
|
+
}
|
|
1393
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1394
|
+
return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
|
|
1367
1395
|
}
|
|
1368
1396
|
case "noScale":
|
|
1369
1397
|
case "noScaleOrReflection": {
|
|
@@ -1389,7 +1417,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1389
1417
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1390
1418
|
st.shearX = 0;
|
|
1391
1419
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1392
|
-
|
|
1420
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1421
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1422
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1423
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1424
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1425
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1426
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1427
|
+
}
|
|
1428
|
+
st.world = world;
|
|
1429
|
+
updateSubtree(skel, bone.id, false);
|
|
1393
1430
|
}
|
|
1394
1431
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1395
1432
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -2148,7 +2185,7 @@ function removeSlot(doc, id) {
|
|
|
2148
2185
|
return patchDrawOrder({
|
|
2149
2186
|
...doc,
|
|
2150
2187
|
slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
|
|
2151
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2188
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2152
2189
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2153
2190
|
}, (o) => o.filter((s) => s !== id));
|
|
2154
2191
|
}
|
|
@@ -2544,7 +2581,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2544
2581
|
return {
|
|
2545
2582
|
...doc,
|
|
2546
2583
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2547
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2584
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2548
2585
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2549
2586
|
};
|
|
2550
2587
|
}
|
|
@@ -4157,6 +4194,9 @@ var Ids = class {
|
|
|
4157
4194
|
has(name) {
|
|
4158
4195
|
return this.map.has(name);
|
|
4159
4196
|
}
|
|
4197
|
+
reserve(id) {
|
|
4198
|
+
this.used.add(id);
|
|
4199
|
+
}
|
|
4160
4200
|
};
|
|
4161
4201
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4162
4202
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
@@ -4165,6 +4205,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4165
4205
|
const warnings = [];
|
|
4166
4206
|
const atlas = parseAtlas(atlasText);
|
|
4167
4207
|
const version = String(json.skeleton?.spine ?? "4.0");
|
|
4208
|
+
if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4168
4209
|
const major = parseFloat(version);
|
|
4169
4210
|
const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
|
|
4170
4211
|
const margin = opts.margin ?? 0;
|
|
@@ -4172,6 +4213,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4172
4213
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4173
4214
|
const boneIds = new Ids("b_"), slotIds = new Ids("s_"), assetIds = new Ids("a_"), attIds = new Ids("att_"), ikIds = new Ids("ik_"), tcIds = new Ids("tc_"), pcIds = new Ids("pc_"), animIds = new Ids("anim_");
|
|
4174
4215
|
const ROOT = "origin";
|
|
4216
|
+
boneIds.reserve(ROOT);
|
|
4175
4217
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4176
4218
|
const attachmentId = (slot, name) => {
|
|
4177
4219
|
const key = `${slot} ${name}`;
|
|
@@ -4196,13 +4238,17 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4196
4238
|
});
|
|
4197
4239
|
}
|
|
4198
4240
|
const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4241
|
+
const selectedSkin = opts.skin ? skins.find((s) => s.name === opts.skin) : skins.find((s) => s.name === "default") ?? skins[0];
|
|
4242
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s) => s.name).join(", ")}`);
|
|
4243
|
+
const lookupSkins = [selectedSkin, skins.find((s) => s.name === "default")].filter((s, i, list) => s && list.indexOf(s) === i);
|
|
4244
|
+
if (skins.length > 1) warnings.push(`Spine skin "${selectedSkin?.name}" selected; setup and attachment timelines are flattened to this skin with default fallback (no runtime skin switching)`);
|
|
4199
4245
|
const frameSuffix = (seq, index) => {
|
|
4200
|
-
const n = String((seq.start ??
|
|
4246
|
+
const n = String((seq.start ?? 1) + index);
|
|
4201
4247
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4202
4248
|
};
|
|
4203
4249
|
const sequences = /* @__PURE__ */ new Map();
|
|
4204
4250
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4205
|
-
for (const skin2 of
|
|
4251
|
+
for (const skin2 of lookupSkins) {
|
|
4206
4252
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4207
4253
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4208
4254
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4211,7 +4257,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4211
4257
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4212
4258
|
}
|
|
4213
4259
|
}
|
|
4214
|
-
const attachmentRef = (slot, name) =>
|
|
4260
|
+
const attachmentRef = (slot, name) => {
|
|
4261
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4262
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4263
|
+
};
|
|
4215
4264
|
const spineSlots = json.slots ?? [];
|
|
4216
4265
|
const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
|
|
4217
4266
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
@@ -4219,11 +4268,12 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4219
4268
|
id: slotIds.get(s.name),
|
|
4220
4269
|
bone: boneIds.get(s.bone),
|
|
4221
4270
|
z: i,
|
|
4222
|
-
...s.attachment ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4271
|
+
...s.attachment && attachmentRef(s.name, s.attachment) ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4223
4272
|
blend: blendOf(s.blend),
|
|
4224
4273
|
color: hex8(s.color),
|
|
4225
4274
|
alpha: 1
|
|
4226
4275
|
}));
|
|
4276
|
+
if (spineSlots.some((s) => s.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4227
4277
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4228
4278
|
const assets = /* @__PURE__ */ new Map();
|
|
4229
4279
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4249,6 +4299,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4249
4299
|
return id;
|
|
4250
4300
|
};
|
|
4251
4301
|
const attachments = [];
|
|
4302
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4252
4303
|
const readVertices = (a, vertexCount2) => {
|
|
4253
4304
|
const v = a.vertices ?? [];
|
|
4254
4305
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4269,6 +4320,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4269
4320
|
}
|
|
4270
4321
|
return { weights };
|
|
4271
4322
|
};
|
|
4323
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4324
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4325
|
+
if (visiting.has(key)) {
|
|
4326
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4327
|
+
return void 0;
|
|
4328
|
+
}
|
|
4329
|
+
const raw = skins.find((s) => s.name === skinName)?.attachments?.[slot]?.[name];
|
|
4330
|
+
if (!raw) {
|
|
4331
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4332
|
+
return void 0;
|
|
4333
|
+
}
|
|
4334
|
+
if (!raw.parent) return raw;
|
|
4335
|
+
visiting.add(key);
|
|
4336
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4337
|
+
};
|
|
4338
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4272
4339
|
for (const skin2 of skins) {
|
|
4273
4340
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4274
4341
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4280,7 +4347,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4280
4347
|
for (const [name, a] of Object.entries(atts)) {
|
|
4281
4348
|
if (a.sequence) {
|
|
4282
4349
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4283
|
-
const base = a.path ?? name;
|
|
4350
|
+
const base = a.path ?? a.name ?? name;
|
|
4284
4351
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4285
4352
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4286
4353
|
if (!asset) continue;
|
|
@@ -4303,7 +4370,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4303
4370
|
}
|
|
4304
4371
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4305
4372
|
if (type === "region") {
|
|
4306
|
-
const asset = assetFor(a.path ?? name);
|
|
4373
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4307
4374
|
if (!asset) continue;
|
|
4308
4375
|
attachments.push({
|
|
4309
4376
|
id,
|
|
@@ -4319,21 +4386,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4319
4386
|
...a.height ? { height: a.height } : {}
|
|
4320
4387
|
});
|
|
4321
4388
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
}
|
|
4326
|
-
const asset = assetFor(a.path ?? name);
|
|
4389
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4390
|
+
if (!geometry) continue;
|
|
4391
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4327
4392
|
if (!asset) continue;
|
|
4328
|
-
const n = (
|
|
4329
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4393
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4394
|
+
attachments.push({ id, slot, type: "mesh", asset, uvs: geometry.uvs, triangles: geometry.triangles, ...readVertices(geometry, n), hull: geometry.hull ?? 0, ...geometry.width ? { width: geometry.width } : {}, ...geometry.height ? { height: geometry.height } : {} });
|
|
4395
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4396
|
+
const parentSkin = a.skin ?? "default";
|
|
4397
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4398
|
+
}
|
|
4330
4399
|
} else if (type === "path") {
|
|
4331
4400
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4401
|
+
} else if (type === "clipping") {
|
|
4402
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4403
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4404
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4405
|
+
[[i, 1]],
|
|
4406
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4407
|
+
]).flat();
|
|
4408
|
+
let geometry;
|
|
4409
|
+
if (data.weights) {
|
|
4410
|
+
const starts = [];
|
|
4411
|
+
let offset = 0;
|
|
4412
|
+
for (const list of data.weights) {
|
|
4413
|
+
starts.push(offset);
|
|
4414
|
+
offset += list.length * 2;
|
|
4415
|
+
}
|
|
4416
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4417
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4418
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4419
|
+
} else {
|
|
4420
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4421
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4422
|
+
clipDeforms.set(id, expand);
|
|
4423
|
+
}
|
|
4424
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4332
4425
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4333
4426
|
}
|
|
4334
4427
|
}
|
|
4335
4428
|
}
|
|
4336
|
-
const
|
|
4429
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s) => s[kind]?.includes(c.name));
|
|
4430
|
+
const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4337
4431
|
id: ikIds.get(c.name),
|
|
4338
4432
|
order: c.order ?? 0,
|
|
4339
4433
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4346,7 +4440,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4346
4440
|
stretch: !!c.stretch,
|
|
4347
4441
|
uniform: !!c.uniform
|
|
4348
4442
|
}));
|
|
4349
|
-
const transform = (json.transform ?? []).map((c) => ({
|
|
4443
|
+
const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4350
4444
|
id: tcIds.get(c.name),
|
|
4351
4445
|
order: c.order ?? 0,
|
|
4352
4446
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4366,7 +4460,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4366
4460
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4367
4461
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4368
4462
|
}));
|
|
4369
|
-
const path = (json.path ?? []).map((c) => ({
|
|
4463
|
+
const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4370
4464
|
id: pcIds.get(c.name),
|
|
4371
4465
|
order: c.order ?? 0,
|
|
4372
4466
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4401,7 +4495,21 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4401
4495
|
for (let i = 0; i < frames.length; i++) {
|
|
4402
4496
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4403
4497
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4404
|
-
|
|
4498
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4499
|
+
const value = f[field] ?? def;
|
|
4500
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4501
|
+
const cubic = (p0, p1, p2, p3, u) => (1 - u) ** 3 * p0 + 3 * (1 - u) ** 2 * u * p1 + 3 * (1 - u) * u * u * p2 + u ** 3 * p3;
|
|
4502
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4503
|
+
const u = (k + 1) / 10;
|
|
4504
|
+
return { t: r4(cubic(t, curve[0], curve[2], next.time ?? 0, u)), v: r4(abs(cubic(value, curve[1], curve[3], value, u))), ease: "linear" };
|
|
4505
|
+
});
|
|
4506
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4507
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4511
|
+
}
|
|
4512
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4405
4513
|
}
|
|
4406
4514
|
return keys;
|
|
4407
4515
|
};
|
|
@@ -4412,6 +4520,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4412
4520
|
const seen = (frames) => {
|
|
4413
4521
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4414
4522
|
};
|
|
4523
|
+
if (a.physics) {
|
|
4524
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4525
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4526
|
+
}
|
|
4415
4527
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4416
4528
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4417
4529
|
if (!sb) {
|
|
@@ -4452,19 +4564,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4452
4564
|
if (kind === "attachment") {
|
|
4453
4565
|
attachmentFrames.set(slotName, frames);
|
|
4454
4566
|
tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
|
|
4455
|
-
} else if (
|
|
4567
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4568
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4569
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4456
4570
|
const rgbaOf = (f) => {
|
|
4457
|
-
const c = hex8(f.color);
|
|
4458
|
-
return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4571
|
+
const c = hex8(f.color ?? f.light);
|
|
4572
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4459
4573
|
};
|
|
4574
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4460
4575
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4461
4576
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4462
|
-
return {
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4577
|
+
return {
|
|
4578
|
+
t: r4(f.time ?? 0),
|
|
4579
|
+
v: rgba.map(r4),
|
|
4580
|
+
ease: "linear",
|
|
4581
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4582
|
+
};
|
|
4468
4583
|
})));
|
|
4469
4584
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4470
4585
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4503,15 +4618,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4503
4618
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4504
4619
|
continue;
|
|
4505
4620
|
}
|
|
4506
|
-
const
|
|
4621
|
+
const rawClip = skins.find((s) => s.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4622
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4623
|
+
const data = clipData ?? att;
|
|
4624
|
+
const length = data.weights ? 2 * data.weights.reduce((s, l) => s + l.length, 0) : data.vertices?.length ?? 0;
|
|
4507
4625
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4508
4626
|
const full = new Array(length).fill(0);
|
|
4509
4627
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4510
4628
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4511
4629
|
const next = frames[i + 1];
|
|
4512
|
-
return { t: r4(f.time ?? 0), v: full.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4630
|
+
return { t: r4(f.time ?? 0), v: (clipDeforms.get(id)?.(full) ?? full).map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4513
4631
|
})));
|
|
4514
4632
|
}
|
|
4633
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4634
|
+
let changed = false;
|
|
4635
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4636
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4637
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4638
|
+
if (source) {
|
|
4639
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4640
|
+
changed = true;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
if (!changed) break;
|
|
4644
|
+
}
|
|
4515
4645
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4516
4646
|
if (Array.isArray(drawOrderFrames)) {
|
|
4517
4647
|
seen(drawOrderFrames);
|
|
@@ -4534,20 +4664,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4534
4664
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4535
4665
|
seen(frames);
|
|
4536
4666
|
const id = ikIds.get(name);
|
|
4667
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4537
4668
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4538
4669
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4539
4670
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4540
4671
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4541
4672
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4542
4673
|
}
|
|
4543
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4674
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4675
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4676
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4677
|
+
t: r4(f.time ?? 0),
|
|
4678
|
+
v: r4(value(f)),
|
|
4679
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4680
|
+
})));
|
|
4681
|
+
};
|
|
4548
4682
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4549
4683
|
seen(frames);
|
|
4550
4684
|
const id = tcIds.get(name);
|
|
4685
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4551
4686
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4552
4687
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4553
4688
|
}
|
|
@@ -4555,6 +4690,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4555
4690
|
const id = pcIds.get(name);
|
|
4556
4691
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4557
4692
|
seen(frames);
|
|
4693
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4558
4694
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4559
4695
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4560
4696
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4717,21 +4853,27 @@ async function importSpineProject(opts) {
|
|
|
4717
4853
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4718
4854
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4719
4855
|
const { pages } = parseAtlas(atlasText);
|
|
4720
|
-
if (pages.length
|
|
4721
|
-
|
|
4722
|
-
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
|
|
4856
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4857
|
+
if (opts.image && pages.length !== 1) throw new Error("image override requires a single-page atlas; multi-page atlases resolve each named page beside the atlas");
|
|
4723
4858
|
const rigDir = dirname6(opts.rigPath);
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4859
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4860
|
+
for (const page2 of pages) {
|
|
4861
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4862
|
+
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), page2.name), pageStem + ".png", pageStem + ".ktx2", ...pages.length === 1 ? [stem + ".png", stem + ".ktx2"] : []], `atlas page image ${page2.name}`);
|
|
4863
|
+
let pngPath = imagePath;
|
|
4864
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4865
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4866
|
+
if (!await exists(pngPath)) {
|
|
4867
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4868
|
+
await writeFile5(pngPath, png);
|
|
4869
|
+
}
|
|
4730
4870
|
}
|
|
4871
|
+
pagePaths.set(page2.name, pngPath);
|
|
4731
4872
|
}
|
|
4732
|
-
const pageImage = relative(rigDir,
|
|
4873
|
+
const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4733
4874
|
const name = opts.name ?? basename4(stem);
|
|
4734
|
-
|
|
4875
|
+
const imagePaths = [...pagePaths.values()];
|
|
4876
|
+
return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4735
4877
|
}
|
|
4736
4878
|
|
|
4737
4879
|
// src/rig-templates.ts
|
|
@@ -5264,13 +5406,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5264
5406
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5265
5407
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5266
5408
|
const id = `attach_${bone}_${target}`;
|
|
5409
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5267
5410
|
const offset = {
|
|
5268
5411
|
x,
|
|
5269
5412
|
y,
|
|
5270
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5413
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5271
5414
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5272
5415
|
scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
|
|
5273
|
-
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI)
|
|
5416
|
+
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI) * handedness
|
|
5274
5417
|
};
|
|
5275
5418
|
const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
|
|
5276
5419
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5533,10 +5676,11 @@ tool(
|
|
|
5533
5676
|
var KeyIn = z3.object({
|
|
5534
5677
|
t: z3.number().describe("seconds"),
|
|
5535
5678
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5679
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5536
5680
|
ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
|
|
5537
5681
|
});
|
|
5538
5682
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5539
|
-
var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5683
|
+
var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5540
5684
|
tool(
|
|
5541
5685
|
"rig_create",
|
|
5542
5686
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5660,19 +5804,20 @@ tool(
|
|
|
5660
5804
|
);
|
|
5661
5805
|
tool(
|
|
5662
5806
|
"rig_import_spine",
|
|
5663
|
-
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text;
|
|
5807
|
+
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
|
|
5664
5808
|
{
|
|
5665
5809
|
path: Path,
|
|
5666
5810
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5667
5811
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5668
5812
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5813
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5669
5814
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5670
5815
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5671
5816
|
notes: z3.string().optional()
|
|
5672
5817
|
},
|
|
5673
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5818
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5674
5819
|
try {
|
|
5675
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5820
|
+
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
|
|
5676
5821
|
const res = await create(path, parse(r.doc));
|
|
5677
5822
|
if (r.warnings.length) res.text += `
|
|
5678
5823
|
warnings:
|