@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
package/dist/lib/cli.js
CHANGED
|
@@ -176,7 +176,9 @@ var PathAttachment = z.object({
|
|
|
176
176
|
/** curve lengths for constant-speed positioning */
|
|
177
177
|
lengths: z.array(z.number()).default([]),
|
|
178
178
|
closed: z.boolean().default(false),
|
|
179
|
-
constantSpeed: z.boolean().default(true)
|
|
179
|
+
constantSpeed: z.boolean().default(true),
|
|
180
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
181
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
180
182
|
});
|
|
181
183
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
182
184
|
var IkConstraint = z.object({
|
|
@@ -245,7 +247,9 @@ var Key = z.object({
|
|
|
245
247
|
t: z.number().nonnegative(),
|
|
246
248
|
v: KeyValue,
|
|
247
249
|
/** easing INTO the next key */
|
|
248
|
-
ease: Ease.default("ease_in_out")
|
|
250
|
+
ease: Ease.default("ease_in_out"),
|
|
251
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
252
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
249
253
|
});
|
|
250
254
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
251
255
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -432,6 +436,10 @@ function validate(doc) {
|
|
|
432
436
|
else if (a.slot !== s2.id) errs.push(`slot "${s2.id}": attachment "${s2.attachment}" belongs to slot "${a.slot}"`);
|
|
433
437
|
}
|
|
434
438
|
}
|
|
439
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
440
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
441
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
442
|
+
}
|
|
435
443
|
const vertexCount2 = (a) => {
|
|
436
444
|
if (a.type === "region") return 4;
|
|
437
445
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -515,6 +523,7 @@ function validate(doc) {
|
|
|
515
523
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
516
524
|
let prev = -1;
|
|
517
525
|
for (const k of t.keys) {
|
|
526
|
+
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`);
|
|
518
527
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
519
528
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
520
529
|
if (t.target === "control") {
|
|
@@ -742,8 +751,8 @@ function sample(track, t, loop = false) {
|
|
|
742
751
|
}
|
|
743
752
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
744
753
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
745
|
-
const
|
|
746
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
754
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
755
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
747
756
|
}
|
|
748
757
|
return a.v;
|
|
749
758
|
}
|
|
@@ -835,6 +844,13 @@ function boneWorldOf(bone, s2, parent) {
|
|
|
835
844
|
if (!parent) return local;
|
|
836
845
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
837
846
|
const [tx, ty] = apply(parent, s2.x, s2.y);
|
|
847
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
848
|
+
const r = s2.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
|
|
849
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
850
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
851
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
852
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s2.scaleX, s2.scaleY, s2.shearX, s2.shearY)), tx, ty };
|
|
853
|
+
}
|
|
838
854
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
839
855
|
}
|
|
840
856
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -867,11 +883,11 @@ var localOf = (doc, pose, b) => {
|
|
|
867
883
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
868
884
|
};
|
|
869
885
|
};
|
|
870
|
-
function updateSubtree(skel, root3) {
|
|
886
|
+
function updateSubtree(skel, root3, includeRoot = true) {
|
|
871
887
|
const doc = skel.doc;
|
|
872
888
|
const kids = /* @__PURE__ */ new Map();
|
|
873
889
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
874
|
-
const stack = [root3];
|
|
890
|
+
const stack = includeRoot ? [root3] : (kids.get(root3) ?? []).map((b) => b.id);
|
|
875
891
|
while (stack.length) {
|
|
876
892
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
877
893
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1114,7 +1130,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1114
1130
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1115
1131
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1116
1132
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1117
|
-
const
|
|
1133
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1134
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1118
1135
|
for (const id of c.bones) {
|
|
1119
1136
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1120
1137
|
if (c.local) {
|
|
@@ -1276,6 +1293,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1276
1293
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1277
1294
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1278
1295
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1296
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1279
1297
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1280
1298
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1281
1299
|
let sum = 0;
|
|
@@ -1310,9 +1328,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1310
1328
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1311
1329
|
}
|
|
1312
1330
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1331
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1313
1332
|
for (let i = 0; i < bones.length; i++) {
|
|
1314
1333
|
const { b, st } = bones[i];
|
|
1315
|
-
let w = { ...
|
|
1334
|
+
let w = { ...inputWorlds[i] };
|
|
1316
1335
|
w.tx += (boneX - w.tx) * mixX;
|
|
1317
1336
|
w.ty += (boneY - w.ty) * mixY;
|
|
1318
1337
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1342,7 +1361,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1342
1361
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1343
1362
|
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 };
|
|
1344
1363
|
}
|
|
1345
|
-
|
|
1364
|
+
solvedWorlds.set(b.id, w);
|
|
1365
|
+
}
|
|
1366
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1367
|
+
const world = solvedWorlds.get(b.id);
|
|
1368
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1346
1369
|
}
|
|
1347
1370
|
}
|
|
1348
1371
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1371,9 +1394,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1371
1394
|
case "onlyTranslation":
|
|
1372
1395
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1373
1396
|
case "noRotationOrReflection": {
|
|
1374
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1397
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1375
1398
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1376
|
-
|
|
1399
|
+
if (lenX * lenX <= 1e-4) {
|
|
1400
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1401
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1402
|
+
}
|
|
1403
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1404
|
+
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 };
|
|
1377
1405
|
}
|
|
1378
1406
|
case "noScale":
|
|
1379
1407
|
case "noScaleOrReflection": {
|
|
@@ -1399,7 +1427,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1399
1427
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1400
1428
|
st.shearX = 0;
|
|
1401
1429
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1402
|
-
|
|
1430
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1431
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1432
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1433
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1434
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1435
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1436
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1437
|
+
}
|
|
1438
|
+
st.world = world;
|
|
1439
|
+
updateSubtree(skel, bone.id, false);
|
|
1403
1440
|
}
|
|
1404
1441
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1405
1442
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -2158,7 +2195,7 @@ function removeSlot(doc, id) {
|
|
|
2158
2195
|
return patchDrawOrder({
|
|
2159
2196
|
...doc,
|
|
2160
2197
|
slots: doc.slots.filter((s2) => s2.id !== id).map((s2) => gone.has(s2.clip ?? "") ? { ...s2, clip: void 0 } : s2),
|
|
2161
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2198
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2162
2199
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2163
2200
|
}, (o) => o.filter((s2) => s2 !== id));
|
|
2164
2201
|
}
|
|
@@ -2554,7 +2591,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2554
2591
|
return {
|
|
2555
2592
|
...doc,
|
|
2556
2593
|
slots: doc.slots.map((s2) => ({ ...s2, id: sub(s2.id) })),
|
|
2557
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2594
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2558
2595
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2559
2596
|
};
|
|
2560
2597
|
}
|
|
@@ -4167,6 +4204,9 @@ var Ids = class {
|
|
|
4167
4204
|
has(name) {
|
|
4168
4205
|
return this.map.has(name);
|
|
4169
4206
|
}
|
|
4207
|
+
reserve(id) {
|
|
4208
|
+
this.used.add(id);
|
|
4209
|
+
}
|
|
4170
4210
|
};
|
|
4171
4211
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4172
4212
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
@@ -4175,6 +4215,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4175
4215
|
const warnings = [];
|
|
4176
4216
|
const atlas = parseAtlas(atlasText);
|
|
4177
4217
|
const version = String(json.skeleton?.spine ?? "4.0");
|
|
4218
|
+
if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4178
4219
|
const major = parseFloat(version);
|
|
4179
4220
|
const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
|
|
4180
4221
|
const margin = opts.margin ?? 0;
|
|
@@ -4182,6 +4223,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4182
4223
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4183
4224
|
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_");
|
|
4184
4225
|
const ROOT = "origin";
|
|
4226
|
+
boneIds.reserve(ROOT);
|
|
4185
4227
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4186
4228
|
const attachmentId = (slot, name) => {
|
|
4187
4229
|
const key = `${slot} ${name}`;
|
|
@@ -4206,13 +4248,17 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4206
4248
|
});
|
|
4207
4249
|
}
|
|
4208
4250
|
const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4251
|
+
const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
|
|
4252
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
|
|
4253
|
+
const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
|
|
4254
|
+
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)`);
|
|
4209
4255
|
const frameSuffix = (seq, index) => {
|
|
4210
|
-
const n = String((seq.start ??
|
|
4256
|
+
const n = String((seq.start ?? 1) + index);
|
|
4211
4257
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4212
4258
|
};
|
|
4213
4259
|
const sequences = /* @__PURE__ */ new Map();
|
|
4214
4260
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4215
|
-
for (const skin2 of
|
|
4261
|
+
for (const skin2 of lookupSkins) {
|
|
4216
4262
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4217
4263
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4218
4264
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4221,7 +4267,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4221
4267
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4222
4268
|
}
|
|
4223
4269
|
}
|
|
4224
|
-
const attachmentRef = (slot, name) =>
|
|
4270
|
+
const attachmentRef = (slot, name) => {
|
|
4271
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4272
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4273
|
+
};
|
|
4225
4274
|
const spineSlots = json.slots ?? [];
|
|
4226
4275
|
const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
|
|
4227
4276
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
@@ -4229,11 +4278,12 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4229
4278
|
id: slotIds.get(s2.name),
|
|
4230
4279
|
bone: boneIds.get(s2.bone),
|
|
4231
4280
|
z: i,
|
|
4232
|
-
...s2.attachment ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4281
|
+
...s2.attachment && attachmentRef(s2.name, s2.attachment) ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4233
4282
|
blend: blendOf(s2.blend),
|
|
4234
4283
|
color: hex8(s2.color),
|
|
4235
4284
|
alpha: 1
|
|
4236
4285
|
}));
|
|
4286
|
+
if (spineSlots.some((s2) => s2.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4237
4287
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4238
4288
|
const assets = /* @__PURE__ */ new Map();
|
|
4239
4289
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4259,6 +4309,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4259
4309
|
return id;
|
|
4260
4310
|
};
|
|
4261
4311
|
const attachments = [];
|
|
4312
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4262
4313
|
const readVertices = (a, vertexCount2) => {
|
|
4263
4314
|
const v = a.vertices ?? [];
|
|
4264
4315
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4279,6 +4330,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4279
4330
|
}
|
|
4280
4331
|
return { weights };
|
|
4281
4332
|
};
|
|
4333
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4334
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4335
|
+
if (visiting.has(key)) {
|
|
4336
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4337
|
+
return void 0;
|
|
4338
|
+
}
|
|
4339
|
+
const raw = skins.find((s2) => s2.name === skinName)?.attachments?.[slot]?.[name];
|
|
4340
|
+
if (!raw) {
|
|
4341
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4342
|
+
return void 0;
|
|
4343
|
+
}
|
|
4344
|
+
if (!raw.parent) return raw;
|
|
4345
|
+
visiting.add(key);
|
|
4346
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4347
|
+
};
|
|
4348
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4282
4349
|
for (const skin2 of skins) {
|
|
4283
4350
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4284
4351
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4290,7 +4357,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4290
4357
|
for (const [name, a] of Object.entries(atts)) {
|
|
4291
4358
|
if (a.sequence) {
|
|
4292
4359
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4293
|
-
const base = a.path ?? name;
|
|
4360
|
+
const base = a.path ?? a.name ?? name;
|
|
4294
4361
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4295
4362
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4296
4363
|
if (!asset) continue;
|
|
@@ -4313,7 +4380,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4313
4380
|
}
|
|
4314
4381
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4315
4382
|
if (type === "region") {
|
|
4316
|
-
const asset = assetFor(a.path ?? name);
|
|
4383
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4317
4384
|
if (!asset) continue;
|
|
4318
4385
|
attachments.push({
|
|
4319
4386
|
id,
|
|
@@ -4329,21 +4396,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4329
4396
|
...a.height ? { height: a.height } : {}
|
|
4330
4397
|
});
|
|
4331
4398
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
}
|
|
4336
|
-
const asset = assetFor(a.path ?? name);
|
|
4399
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4400
|
+
if (!geometry) continue;
|
|
4401
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4337
4402
|
if (!asset) continue;
|
|
4338
|
-
const n = (
|
|
4339
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4403
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4404
|
+
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 } : {} });
|
|
4405
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4406
|
+
const parentSkin = a.skin ?? "default";
|
|
4407
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4408
|
+
}
|
|
4340
4409
|
} else if (type === "path") {
|
|
4341
4410
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4411
|
+
} else if (type === "clipping") {
|
|
4412
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4413
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4414
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4415
|
+
[[i, 1]],
|
|
4416
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4417
|
+
]).flat();
|
|
4418
|
+
let geometry;
|
|
4419
|
+
if (data.weights) {
|
|
4420
|
+
const starts = [];
|
|
4421
|
+
let offset = 0;
|
|
4422
|
+
for (const list of data.weights) {
|
|
4423
|
+
starts.push(offset);
|
|
4424
|
+
offset += list.length * 2;
|
|
4425
|
+
}
|
|
4426
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4427
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4428
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4429
|
+
} else {
|
|
4430
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4431
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4432
|
+
clipDeforms.set(id, expand);
|
|
4433
|
+
}
|
|
4434
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4342
4435
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4343
4436
|
}
|
|
4344
4437
|
}
|
|
4345
4438
|
}
|
|
4346
|
-
const
|
|
4439
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
|
|
4440
|
+
const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4347
4441
|
id: ikIds.get(c.name),
|
|
4348
4442
|
order: c.order ?? 0,
|
|
4349
4443
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4356,7 +4450,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4356
4450
|
stretch: !!c.stretch,
|
|
4357
4451
|
uniform: !!c.uniform
|
|
4358
4452
|
}));
|
|
4359
|
-
const transform = (json.transform ?? []).map((c) => ({
|
|
4453
|
+
const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4360
4454
|
id: tcIds.get(c.name),
|
|
4361
4455
|
order: c.order ?? 0,
|
|
4362
4456
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4376,7 +4470,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4376
4470
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4377
4471
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4378
4472
|
}));
|
|
4379
|
-
const path = (json.path ?? []).map((c) => ({
|
|
4473
|
+
const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4380
4474
|
id: pcIds.get(c.name),
|
|
4381
4475
|
order: c.order ?? 0,
|
|
4382
4476
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4411,7 +4505,21 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4411
4505
|
for (let i = 0; i < frames.length; i++) {
|
|
4412
4506
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4413
4507
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4414
|
-
|
|
4508
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4509
|
+
const value = f[field] ?? def;
|
|
4510
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4511
|
+
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;
|
|
4512
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4513
|
+
const u = (k + 1) / 10;
|
|
4514
|
+
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" };
|
|
4515
|
+
});
|
|
4516
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4517
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4521
|
+
}
|
|
4522
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4415
4523
|
}
|
|
4416
4524
|
return keys;
|
|
4417
4525
|
};
|
|
@@ -4422,6 +4530,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4422
4530
|
const seen = (frames) => {
|
|
4423
4531
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4424
4532
|
};
|
|
4533
|
+
if (a.physics) {
|
|
4534
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4535
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4536
|
+
}
|
|
4425
4537
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4426
4538
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4427
4539
|
if (!sb) {
|
|
@@ -4462,19 +4574,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4462
4574
|
if (kind === "attachment") {
|
|
4463
4575
|
attachmentFrames.set(slotName, frames);
|
|
4464
4576
|
tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
|
|
4465
|
-
} else if (
|
|
4577
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4578
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4579
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4466
4580
|
const rgbaOf = (f) => {
|
|
4467
|
-
const c = hex8(f.color);
|
|
4468
|
-
return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4581
|
+
const c = hex8(f.color ?? f.light);
|
|
4582
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4469
4583
|
};
|
|
4584
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4470
4585
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4471
4586
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4472
|
-
return {
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4587
|
+
return {
|
|
4588
|
+
t: r4(f.time ?? 0),
|
|
4589
|
+
v: rgba.map(r4),
|
|
4590
|
+
ease: "linear",
|
|
4591
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4592
|
+
};
|
|
4478
4593
|
})));
|
|
4479
4594
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4480
4595
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4513,15 +4628,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4513
4628
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4514
4629
|
continue;
|
|
4515
4630
|
}
|
|
4516
|
-
const
|
|
4631
|
+
const rawClip = skins.find((s2) => s2.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4632
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4633
|
+
const data = clipData ?? att;
|
|
4634
|
+
const length = data.weights ? 2 * data.weights.reduce((s2, l) => s2 + l.length, 0) : data.vertices?.length ?? 0;
|
|
4517
4635
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4518
4636
|
const full = new Array(length).fill(0);
|
|
4519
4637
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4520
4638
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4521
4639
|
const next = frames[i + 1];
|
|
4522
|
-
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" };
|
|
4640
|
+
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" };
|
|
4523
4641
|
})));
|
|
4524
4642
|
}
|
|
4643
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4644
|
+
let changed = false;
|
|
4645
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4646
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4647
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4648
|
+
if (source) {
|
|
4649
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4650
|
+
changed = true;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (!changed) break;
|
|
4654
|
+
}
|
|
4525
4655
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4526
4656
|
if (Array.isArray(drawOrderFrames)) {
|
|
4527
4657
|
seen(drawOrderFrames);
|
|
@@ -4544,20 +4674,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4544
4674
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4545
4675
|
seen(frames);
|
|
4546
4676
|
const id = ikIds.get(name);
|
|
4677
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4547
4678
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4548
4679
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4549
4680
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4550
4681
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4551
4682
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4552
4683
|
}
|
|
4553
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4684
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4685
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4686
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4687
|
+
t: r4(f.time ?? 0),
|
|
4688
|
+
v: r4(value(f)),
|
|
4689
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4690
|
+
})));
|
|
4691
|
+
};
|
|
4558
4692
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4559
4693
|
seen(frames);
|
|
4560
4694
|
const id = tcIds.get(name);
|
|
4695
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4561
4696
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4562
4697
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4563
4698
|
}
|
|
@@ -4565,6 +4700,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4565
4700
|
const id = pcIds.get(name);
|
|
4566
4701
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4567
4702
|
seen(frames);
|
|
4703
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4568
4704
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4569
4705
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4570
4706
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4727,21 +4863,27 @@ async function importSpineProject(opts) {
|
|
|
4727
4863
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4728
4864
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4729
4865
|
const { pages } = parseAtlas(atlasText);
|
|
4730
|
-
if (pages.length
|
|
4731
|
-
|
|
4732
|
-
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
|
|
4866
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4867
|
+
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");
|
|
4733
4868
|
const rigDir = dirname6(opts.rigPath);
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4869
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4870
|
+
for (const page2 of pages) {
|
|
4871
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4872
|
+
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}`);
|
|
4873
|
+
let pngPath = imagePath;
|
|
4874
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4875
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4876
|
+
if (!await exists(pngPath)) {
|
|
4877
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4878
|
+
await writeFile5(pngPath, png);
|
|
4879
|
+
}
|
|
4740
4880
|
}
|
|
4881
|
+
pagePaths.set(page2.name, pngPath);
|
|
4741
4882
|
}
|
|
4742
|
-
const pageImage = relative(rigDir,
|
|
4883
|
+
const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4743
4884
|
const name = opts.name ?? basename4(stem);
|
|
4744
|
-
|
|
4885
|
+
const imagePaths = [...pagePaths.values()];
|
|
4886
|
+
return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4745
4887
|
}
|
|
4746
4888
|
|
|
4747
4889
|
// src/rig-templates.ts
|
|
@@ -5274,13 +5416,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5274
5416
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5275
5417
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5276
5418
|
const id = `attach_${bone}_${target}`;
|
|
5419
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5277
5420
|
const offset = {
|
|
5278
5421
|
x,
|
|
5279
5422
|
y,
|
|
5280
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5423
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5281
5424
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5282
5425
|
scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
|
|
5283
|
-
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)
|
|
5426
|
+
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
|
|
5284
5427
|
};
|
|
5285
5428
|
const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
|
|
5286
5429
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5543,10 +5686,11 @@ tool(
|
|
|
5543
5686
|
var KeyIn = z3.object({
|
|
5544
5687
|
t: z3.number().describe("seconds"),
|
|
5545
5688
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5689
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5546
5690
|
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")
|
|
5547
5691
|
});
|
|
5548
5692
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5549
|
-
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].";
|
|
5693
|
+
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].";
|
|
5550
5694
|
tool(
|
|
5551
5695
|
"rig_create",
|
|
5552
5696
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5670,19 +5814,20 @@ tool(
|
|
|
5670
5814
|
);
|
|
5671
5815
|
tool(
|
|
5672
5816
|
"rig_import_spine",
|
|
5673
|
-
"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;
|
|
5817
|
+
"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.",
|
|
5674
5818
|
{
|
|
5675
5819
|
path: Path,
|
|
5676
5820
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5677
5821
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5678
5822
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5823
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5679
5824
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5680
5825
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5681
5826
|
notes: z3.string().optional()
|
|
5682
5827
|
},
|
|
5683
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5828
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5684
5829
|
try {
|
|
5685
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5830
|
+
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 });
|
|
5686
5831
|
const res = await create(path, parse(r.doc));
|
|
5687
5832
|
if (r.warnings.length) res.text += `
|
|
5688
5833
|
warnings:
|