@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/editor-entry.js
CHANGED
|
@@ -173,7 +173,9 @@ var PathAttachment = z.object({
|
|
|
173
173
|
/** curve lengths for constant-speed positioning */
|
|
174
174
|
lengths: z.array(z.number()).default([]),
|
|
175
175
|
closed: z.boolean().default(false),
|
|
176
|
-
constantSpeed: z.boolean().default(true)
|
|
176
|
+
constantSpeed: z.boolean().default(true),
|
|
177
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
178
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
177
179
|
});
|
|
178
180
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
179
181
|
var IkConstraint = z.object({
|
|
@@ -242,7 +244,9 @@ var Key = z.object({
|
|
|
242
244
|
t: z.number().nonnegative(),
|
|
243
245
|
v: KeyValue,
|
|
244
246
|
/** easing INTO the next key */
|
|
245
|
-
ease: Ease.default("ease_in_out")
|
|
247
|
+
ease: Ease.default("ease_in_out"),
|
|
248
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
249
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
246
250
|
});
|
|
247
251
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
248
252
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -429,6 +433,10 @@ function validate(doc) {
|
|
|
429
433
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
430
434
|
}
|
|
431
435
|
}
|
|
436
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
437
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
438
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
439
|
+
}
|
|
432
440
|
const vertexCount2 = (a) => {
|
|
433
441
|
if (a.type === "region") return 4;
|
|
434
442
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -512,6 +520,7 @@ function validate(doc) {
|
|
|
512
520
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
513
521
|
let prev = -1;
|
|
514
522
|
for (const k of t.keys) {
|
|
523
|
+
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`);
|
|
515
524
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
516
525
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
517
526
|
if (t.target === "control") {
|
|
@@ -739,8 +748,8 @@ function sample(track, t, loop = false) {
|
|
|
739
748
|
}
|
|
740
749
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
741
750
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
742
|
-
const
|
|
743
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
751
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
752
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
744
753
|
}
|
|
745
754
|
return a.v;
|
|
746
755
|
}
|
|
@@ -832,6 +841,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
832
841
|
if (!parent) return local;
|
|
833
842
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
834
843
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
844
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
845
|
+
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);
|
|
846
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
847
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
848
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
849
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
850
|
+
}
|
|
835
851
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
836
852
|
}
|
|
837
853
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -864,11 +880,11 @@ var localOf = (doc, pose, b) => {
|
|
|
864
880
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
865
881
|
};
|
|
866
882
|
};
|
|
867
|
-
function updateSubtree(skel, root2) {
|
|
883
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
868
884
|
const doc = skel.doc;
|
|
869
885
|
const kids = /* @__PURE__ */ new Map();
|
|
870
886
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
871
|
-
const stack = [root2];
|
|
887
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
872
888
|
while (stack.length) {
|
|
873
889
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
874
890
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1111,7 +1127,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1111
1127
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1112
1128
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1113
1129
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1114
|
-
const
|
|
1130
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1131
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1115
1132
|
for (const id of c.bones) {
|
|
1116
1133
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1117
1134
|
if (c.local) {
|
|
@@ -1273,6 +1290,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1273
1290
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1274
1291
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1275
1292
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1293
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1276
1294
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1277
1295
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1278
1296
|
let sum = 0;
|
|
@@ -1307,9 +1325,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1307
1325
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1308
1326
|
}
|
|
1309
1327
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1328
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1310
1329
|
for (let i = 0; i < bones.length; i++) {
|
|
1311
1330
|
const { b, st } = bones[i];
|
|
1312
|
-
let w = { ...
|
|
1331
|
+
let w = { ...inputWorlds[i] };
|
|
1313
1332
|
w.tx += (boneX - w.tx) * mixX;
|
|
1314
1333
|
w.ty += (boneY - w.ty) * mixY;
|
|
1315
1334
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1339,7 +1358,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1339
1358
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1340
1359
|
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 };
|
|
1341
1360
|
}
|
|
1342
|
-
|
|
1361
|
+
solvedWorlds.set(b.id, w);
|
|
1362
|
+
}
|
|
1363
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1364
|
+
const world = solvedWorlds.get(b.id);
|
|
1365
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1343
1366
|
}
|
|
1344
1367
|
}
|
|
1345
1368
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1368,9 +1391,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1368
1391
|
case "onlyTranslation":
|
|
1369
1392
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1370
1393
|
case "noRotationOrReflection": {
|
|
1371
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1394
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1372
1395
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1373
|
-
|
|
1396
|
+
if (lenX * lenX <= 1e-4) {
|
|
1397
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1398
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1399
|
+
}
|
|
1400
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1401
|
+
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 };
|
|
1374
1402
|
}
|
|
1375
1403
|
case "noScale":
|
|
1376
1404
|
case "noScaleOrReflection": {
|
|
@@ -1396,7 +1424,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1396
1424
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1397
1425
|
st.shearX = 0;
|
|
1398
1426
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1399
|
-
|
|
1427
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1428
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1429
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1430
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1431
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1432
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1433
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1434
|
+
}
|
|
1435
|
+
st.world = world;
|
|
1436
|
+
updateSubtree(skel, bone.id, false);
|
|
1400
1437
|
}
|
|
1401
1438
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1402
1439
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -2155,7 +2192,7 @@ function removeSlot(doc, id) {
|
|
|
2155
2192
|
return patchDrawOrder({
|
|
2156
2193
|
...doc,
|
|
2157
2194
|
slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
|
|
2158
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2195
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2159
2196
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2160
2197
|
}, (o) => o.filter((s) => s !== id));
|
|
2161
2198
|
}
|
|
@@ -2551,7 +2588,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2551
2588
|
return {
|
|
2552
2589
|
...doc,
|
|
2553
2590
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2554
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2591
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2555
2592
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2556
2593
|
};
|
|
2557
2594
|
}
|
|
@@ -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 });
|
|
@@ -5532,10 +5675,11 @@ tool(
|
|
|
5532
5675
|
var KeyIn = z3.object({
|
|
5533
5676
|
t: z3.number().describe("seconds"),
|
|
5534
5677
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5678
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5535
5679
|
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")
|
|
5536
5680
|
});
|
|
5537
5681
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5538
|
-
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].";
|
|
5682
|
+
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].";
|
|
5539
5683
|
tool(
|
|
5540
5684
|
"rig_create",
|
|
5541
5685
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5659,19 +5803,20 @@ tool(
|
|
|
5659
5803
|
);
|
|
5660
5804
|
tool(
|
|
5661
5805
|
"rig_import_spine",
|
|
5662
|
-
"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;
|
|
5806
|
+
"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.",
|
|
5663
5807
|
{
|
|
5664
5808
|
path: Path,
|
|
5665
5809
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5666
5810
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5667
5811
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5812
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5668
5813
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5669
5814
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5670
5815
|
notes: z3.string().optional()
|
|
5671
5816
|
},
|
|
5672
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5817
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5673
5818
|
try {
|
|
5674
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5819
|
+
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 });
|
|
5675
5820
|
const res = await create(path, parse(r.doc));
|
|
5676
5821
|
if (r.warnings.length) res.text += `
|
|
5677
5822
|
warnings:
|