@energy8platform/golem 0.5.0 → 0.7.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 +18 -0
- package/dist/editor.css +3 -4
- package/dist/editor.js +177 -75
- package/dist/lib/cli.js +302 -126
- package/dist/lib/cli.js.map +4 -4
- package/dist/lib/e8/agent.d.ts +35 -0
- package/dist/lib/e8/host.d.ts +22 -0
- package/dist/lib/e8/node.d.ts +66 -0
- package/dist/lib/e8/runtime.d.ts +10 -0
- package/dist/lib/e8/schema.d.ts +3 -0
- package/dist/lib/e8/types.d.ts +80 -0
- package/dist/lib/e8-agent.js +6781 -0
- package/dist/lib/e8-agent.js.map +7 -0
- package/dist/lib/e8-client.js +116 -0
- package/dist/lib/e8-host.js +6929 -0
- package/dist/lib/e8-host.js.map +7 -0
- package/dist/lib/e8-runtime.js +2024 -0
- package/dist/lib/e8-runtime.js.map +7 -0
- package/dist/lib/e8-schema.js +7 -0
- package/dist/lib/e8-schema.js.map +7 -0
- package/dist/lib/editor/api.d.ts +1 -0
- package/dist/lib/editor/embed.d.ts +9 -0
- package/dist/lib/editor/server.d.ts +23 -1
- package/dist/lib/editor/store.d.ts +107 -0
- package/dist/lib/editor/timeline.d.ts +15 -0
- package/dist/lib/editor-entry.d.ts +1 -1
- package/dist/lib/editor-entry.js +303 -126
- package/dist/lib/editor-entry.js.map +4 -4
- 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/rig-import-layers.d.ts +2 -0
- package/dist/lib/rig-version.d.ts +12 -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/tool-schema.d.ts +3 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +257 -73
- package/dist/lib/tools.js.map +4 -4
- package/editor.html +2 -2
- package/package.json +38 -3
- package/skills/e8-golem/SKILL.md +71 -0
- package/skills/golem-symbol-animation/SKILL.md +87 -0
- package/skills/golem-symbol-animation/agents/openai.yaml +4 -0
- package/skills/golem-symbol-animation/references/facial-controls.md +48 -0
- package/skills/golem-symbol-animation/references/game-engine-integration.md +133 -0
- package/skills/golem-symbol-animation/references/golem-authoring.md +138 -0
- package/skills/golem-symbol-animation/references/interactive-editor.md +17 -0
- package/skills/golem-symbol-animation/references/motion-craft.md +113 -0
- package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
- package/skills/golem-symbol-animation/references/quantitative-qa.md +106 -0
- package/skills/golem-symbol-animation/references/spine-rive-study.md +87 -0
- package/skills/golem-symbol-animation/scripts/atlas_parts.py +87 -0
- package/skills/golem-symbol-animation/scripts/check_package.py +71 -0
- package/skills/golem-symbol-animation/scripts/image_gates.py +143 -0
- package/skills/golem-symbol-cutting/SKILL.md +70 -0
- package/skills/golem-symbol-cutting/agents/openai.yaml +4 -0
- package/skills/golem-symbol-cutting/assets/h1/h1.atlas.png +0 -0
- package/skills/golem-symbol-cutting/assets/h1/h1.png +0 -0
- package/skills/golem-symbol-cutting/references/cutting-workflow.md +92 -0
- package/skills/golem-symbol-cutting/references/facial-layers.md +44 -0
- package/skills/golem-symbol-cutting/references/generation-workflow.md +109 -0
- package/skills/golem-symbol-cutting/references/golem-handoff.md +62 -0
- package/skills/golem-symbol-cutting/references/h1-example.md +46 -0
- package/skills/golem-symbol-cutting/references/hybrid-workflow.md +119 -0
- package/skills/golem-symbol-cutting/references/registration-and-motion.md +89 -0
- package/skills/golem-symbol-cutting/references/spine-rive-construction.md +73 -0
- package/skills/golem-symbol-cutting/scripts/atlas_parts.py +87 -0
- package/skills/golem-symbol-cutting/scripts/hybrid_parts.py +279 -0
package/dist/lib/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/rig-api.ts
|
|
2
2
|
import { z as z3 } from "zod";
|
|
3
3
|
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
4
|
-
import { dirname as dirname8, join as join9, relative as
|
|
4
|
+
import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/rig-io.ts
|
|
7
7
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -24,8 +24,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
|
|
|
24
24
|
// src/rig-queue.ts
|
|
25
25
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
26
|
import { resolve } from "node:path";
|
|
27
|
-
var pending
|
|
28
|
-
|
|
27
|
+
var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
|
|
28
|
+
pending: /* @__PURE__ */ new Map(),
|
|
29
|
+
active: new AsyncLocalStorage()
|
|
30
|
+
};
|
|
29
31
|
async function withRigQueue(path, op) {
|
|
30
32
|
const key = resolve(path);
|
|
31
33
|
const held = active.getStore();
|
|
@@ -216,7 +218,9 @@ var PathAttachment = z.object({
|
|
|
216
218
|
/** curve lengths for constant-speed positioning */
|
|
217
219
|
lengths: z.array(z.number()).default([]),
|
|
218
220
|
closed: z.boolean().default(false),
|
|
219
|
-
constantSpeed: z.boolean().default(true)
|
|
221
|
+
constantSpeed: z.boolean().default(true),
|
|
222
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
223
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
220
224
|
});
|
|
221
225
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
222
226
|
var IkConstraint = z.object({
|
|
@@ -285,7 +289,9 @@ var Key = z.object({
|
|
|
285
289
|
t: z.number().nonnegative(),
|
|
286
290
|
v: KeyValue,
|
|
287
291
|
/** easing INTO the next key */
|
|
288
|
-
ease: Ease.default("ease_in_out")
|
|
292
|
+
ease: Ease.default("ease_in_out"),
|
|
293
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
294
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
289
295
|
});
|
|
290
296
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
291
297
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -472,6 +478,10 @@ function validate(doc) {
|
|
|
472
478
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
473
479
|
}
|
|
474
480
|
}
|
|
481
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
482
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
483
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
484
|
+
}
|
|
475
485
|
const vertexCount2 = (a) => {
|
|
476
486
|
if (a.type === "region") return 4;
|
|
477
487
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -555,6 +565,7 @@ function validate(doc) {
|
|
|
555
565
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
556
566
|
let prev = -1;
|
|
557
567
|
for (const k of t.keys) {
|
|
568
|
+
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
569
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
559
570
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
560
571
|
if (t.target === "control") {
|
|
@@ -732,8 +743,8 @@ function sample(track, t, loop = false) {
|
|
|
732
743
|
}
|
|
733
744
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
734
745
|
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) *
|
|
746
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
747
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
737
748
|
}
|
|
738
749
|
return a.v;
|
|
739
750
|
}
|
|
@@ -825,6 +836,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
825
836
|
if (!parent) return local;
|
|
826
837
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
827
838
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
839
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
840
|
+
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);
|
|
841
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
842
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
843
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
844
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
845
|
+
}
|
|
828
846
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
829
847
|
}
|
|
830
848
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -857,11 +875,11 @@ var localOf = (doc, pose, b) => {
|
|
|
857
875
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
858
876
|
};
|
|
859
877
|
};
|
|
860
|
-
function updateSubtree(skel, root2) {
|
|
878
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
861
879
|
const doc = skel.doc;
|
|
862
880
|
const kids = /* @__PURE__ */ new Map();
|
|
863
881
|
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];
|
|
882
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
865
883
|
while (stack.length) {
|
|
866
884
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
867
885
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1104,7 +1122,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1104
1122
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1105
1123
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1106
1124
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1107
|
-
const
|
|
1125
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1126
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1108
1127
|
for (const id of c.bones) {
|
|
1109
1128
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1110
1129
|
if (c.local) {
|
|
@@ -1266,6 +1285,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1266
1285
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1267
1286
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1268
1287
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1288
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1269
1289
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1270
1290
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1271
1291
|
let sum = 0;
|
|
@@ -1300,9 +1320,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1300
1320
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1301
1321
|
}
|
|
1302
1322
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1323
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1303
1324
|
for (let i = 0; i < bones.length; i++) {
|
|
1304
1325
|
const { b, st } = bones[i];
|
|
1305
|
-
let w = { ...
|
|
1326
|
+
let w = { ...inputWorlds[i] };
|
|
1306
1327
|
w.tx += (boneX - w.tx) * mixX;
|
|
1307
1328
|
w.ty += (boneY - w.ty) * mixY;
|
|
1308
1329
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1332,7 +1353,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1332
1353
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1333
1354
|
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
1355
|
}
|
|
1335
|
-
|
|
1356
|
+
solvedWorlds.set(b.id, w);
|
|
1357
|
+
}
|
|
1358
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1359
|
+
const world = solvedWorlds.get(b.id);
|
|
1360
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1336
1361
|
}
|
|
1337
1362
|
}
|
|
1338
1363
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1361,9 +1386,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1361
1386
|
case "onlyTranslation":
|
|
1362
1387
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1363
1388
|
case "noRotationOrReflection": {
|
|
1364
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1389
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1365
1390
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1366
|
-
|
|
1391
|
+
if (lenX * lenX <= 1e-4) {
|
|
1392
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1393
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1394
|
+
}
|
|
1395
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1396
|
+
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
1397
|
}
|
|
1368
1398
|
case "noScale":
|
|
1369
1399
|
case "noScaleOrReflection": {
|
|
@@ -1389,7 +1419,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1389
1419
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1390
1420
|
st.shearX = 0;
|
|
1391
1421
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1392
|
-
|
|
1422
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1423
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1424
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1425
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1426
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1427
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1428
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1429
|
+
}
|
|
1430
|
+
st.world = world;
|
|
1431
|
+
updateSubtree(skel, bone.id, false);
|
|
1393
1432
|
}
|
|
1394
1433
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1395
1434
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1964,8 +2003,9 @@ function createDoc(name, width, height, notes) {
|
|
|
1964
2003
|
function createSymbol(name, asset, margin = 0.15, notes) {
|
|
1965
2004
|
const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
|
|
1966
2005
|
const width = asset.width + 2 * mx, height = asset.height + 2 * my;
|
|
1967
|
-
const
|
|
1968
|
-
const
|
|
2006
|
+
const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
2007
|
+
const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
|
|
2008
|
+
const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
|
|
1969
2009
|
let doc = createDoc(name, width, height, notes);
|
|
1970
2010
|
doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
|
|
1971
2011
|
doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
|
|
@@ -2148,7 +2188,7 @@ function removeSlot(doc, id) {
|
|
|
2148
2188
|
return patchDrawOrder({
|
|
2149
2189
|
...doc,
|
|
2150
2190
|
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),
|
|
2191
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2152
2192
|
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
2193
|
}, (o) => o.filter((s) => s !== id));
|
|
2154
2194
|
}
|
|
@@ -2544,7 +2584,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2544
2584
|
return {
|
|
2545
2585
|
...doc,
|
|
2546
2586
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2547
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2587
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2548
2588
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2549
2589
|
};
|
|
2550
2590
|
}
|
|
@@ -2721,7 +2761,7 @@ function setGoal(doc, bone, x, y, opts) {
|
|
|
2721
2761
|
|
|
2722
2762
|
// src/rig-import-layers.ts
|
|
2723
2763
|
import { readdir, readFile } from "node:fs/promises";
|
|
2724
|
-
import { join as join2, basename as basename2 } from "node:path";
|
|
2764
|
+
import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
|
|
2725
2765
|
var DEFAULT_PARENT = {
|
|
2726
2766
|
head: "neck",
|
|
2727
2767
|
neck: "torso",
|
|
@@ -2747,6 +2787,10 @@ function pngSize(buf) {
|
|
|
2747
2787
|
if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
|
|
2748
2788
|
return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
|
|
2749
2789
|
}
|
|
2790
|
+
function outsideDir(dir, file) {
|
|
2791
|
+
const r = relative(dir, join2(dir, file));
|
|
2792
|
+
return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
|
|
2793
|
+
}
|
|
2750
2794
|
async function importLayers(doc, dir, relPrefix = "layers") {
|
|
2751
2795
|
let specs;
|
|
2752
2796
|
try {
|
|
@@ -2759,6 +2803,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
|
|
|
2759
2803
|
let next = doc;
|
|
2760
2804
|
const parsed = [];
|
|
2761
2805
|
for (const s of specs) {
|
|
2806
|
+
if (outsideDir(dir, s.file)) throw new Error(`layers.json file "${s.file}" is outside the layers folder (${dir})`);
|
|
2762
2807
|
const buf = await readFile(join2(dir, s.file));
|
|
2763
2808
|
const [w, h] = pngSize(buf);
|
|
2764
2809
|
const m = basename2(s.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
|
|
@@ -4015,7 +4060,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
|
|
|
4015
4060
|
|
|
4016
4061
|
// src/spine-import.ts
|
|
4017
4062
|
import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
|
|
4018
|
-
import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
|
|
4063
|
+
import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
|
|
4019
4064
|
|
|
4020
4065
|
// src/ktx2.ts
|
|
4021
4066
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
@@ -4157,6 +4202,9 @@ var Ids = class {
|
|
|
4157
4202
|
has(name) {
|
|
4158
4203
|
return this.map.has(name);
|
|
4159
4204
|
}
|
|
4205
|
+
reserve(id) {
|
|
4206
|
+
this.used.add(id);
|
|
4207
|
+
}
|
|
4160
4208
|
};
|
|
4161
4209
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4162
4210
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
@@ -4165,6 +4213,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4165
4213
|
const warnings = [];
|
|
4166
4214
|
const atlas = parseAtlas(atlasText);
|
|
4167
4215
|
const version = String(json.skeleton?.spine ?? "4.0");
|
|
4216
|
+
if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4168
4217
|
const major = parseFloat(version);
|
|
4169
4218
|
const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
|
|
4170
4219
|
const margin = opts.margin ?? 0;
|
|
@@ -4172,6 +4221,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4172
4221
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4173
4222
|
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
4223
|
const ROOT = "origin";
|
|
4224
|
+
boneIds.reserve(ROOT);
|
|
4175
4225
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4176
4226
|
const attachmentId = (slot, name) => {
|
|
4177
4227
|
const key = `${slot} ${name}`;
|
|
@@ -4196,13 +4246,17 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4196
4246
|
});
|
|
4197
4247
|
}
|
|
4198
4248
|
const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4249
|
+
const selectedSkin = opts.skin ? skins.find((s) => s.name === opts.skin) : skins.find((s) => s.name === "default") ?? skins[0];
|
|
4250
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s) => s.name).join(", ")}`);
|
|
4251
|
+
const lookupSkins = [selectedSkin, skins.find((s) => s.name === "default")].filter((s, i, list) => s && list.indexOf(s) === i);
|
|
4252
|
+
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
4253
|
const frameSuffix = (seq, index) => {
|
|
4200
|
-
const n = String((seq.start ??
|
|
4254
|
+
const n = String((seq.start ?? 1) + index);
|
|
4201
4255
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4202
4256
|
};
|
|
4203
4257
|
const sequences = /* @__PURE__ */ new Map();
|
|
4204
4258
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4205
|
-
for (const skin2 of
|
|
4259
|
+
for (const skin2 of lookupSkins) {
|
|
4206
4260
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4207
4261
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4208
4262
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4211,7 +4265,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4211
4265
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4212
4266
|
}
|
|
4213
4267
|
}
|
|
4214
|
-
const attachmentRef = (slot, name) =>
|
|
4268
|
+
const attachmentRef = (slot, name) => {
|
|
4269
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4270
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4271
|
+
};
|
|
4215
4272
|
const spineSlots = json.slots ?? [];
|
|
4216
4273
|
const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
|
|
4217
4274
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
@@ -4219,11 +4276,12 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4219
4276
|
id: slotIds.get(s.name),
|
|
4220
4277
|
bone: boneIds.get(s.bone),
|
|
4221
4278
|
z: i,
|
|
4222
|
-
...s.attachment ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4279
|
+
...s.attachment && attachmentRef(s.name, s.attachment) ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4223
4280
|
blend: blendOf(s.blend),
|
|
4224
4281
|
color: hex8(s.color),
|
|
4225
4282
|
alpha: 1
|
|
4226
4283
|
}));
|
|
4284
|
+
if (spineSlots.some((s) => s.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4227
4285
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4228
4286
|
const assets = /* @__PURE__ */ new Map();
|
|
4229
4287
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4249,6 +4307,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4249
4307
|
return id;
|
|
4250
4308
|
};
|
|
4251
4309
|
const attachments = [];
|
|
4310
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4252
4311
|
const readVertices = (a, vertexCount2) => {
|
|
4253
4312
|
const v = a.vertices ?? [];
|
|
4254
4313
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4269,6 +4328,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4269
4328
|
}
|
|
4270
4329
|
return { weights };
|
|
4271
4330
|
};
|
|
4331
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4332
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4333
|
+
if (visiting.has(key)) {
|
|
4334
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4335
|
+
return void 0;
|
|
4336
|
+
}
|
|
4337
|
+
const raw = skins.find((s) => s.name === skinName)?.attachments?.[slot]?.[name];
|
|
4338
|
+
if (!raw) {
|
|
4339
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4340
|
+
return void 0;
|
|
4341
|
+
}
|
|
4342
|
+
if (!raw.parent) return raw;
|
|
4343
|
+
visiting.add(key);
|
|
4344
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4345
|
+
};
|
|
4346
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4272
4347
|
for (const skin2 of skins) {
|
|
4273
4348
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4274
4349
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4280,7 +4355,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4280
4355
|
for (const [name, a] of Object.entries(atts)) {
|
|
4281
4356
|
if (a.sequence) {
|
|
4282
4357
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4283
|
-
const base = a.path ?? name;
|
|
4358
|
+
const base = a.path ?? a.name ?? name;
|
|
4284
4359
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4285
4360
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4286
4361
|
if (!asset) continue;
|
|
@@ -4303,7 +4378,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4303
4378
|
}
|
|
4304
4379
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4305
4380
|
if (type === "region") {
|
|
4306
|
-
const asset = assetFor(a.path ?? name);
|
|
4381
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4307
4382
|
if (!asset) continue;
|
|
4308
4383
|
attachments.push({
|
|
4309
4384
|
id,
|
|
@@ -4319,21 +4394,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4319
4394
|
...a.height ? { height: a.height } : {}
|
|
4320
4395
|
});
|
|
4321
4396
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
}
|
|
4326
|
-
const asset = assetFor(a.path ?? name);
|
|
4397
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4398
|
+
if (!geometry) continue;
|
|
4399
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4327
4400
|
if (!asset) continue;
|
|
4328
|
-
const n = (
|
|
4329
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4401
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4402
|
+
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 } : {} });
|
|
4403
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4404
|
+
const parentSkin = a.skin ?? "default";
|
|
4405
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4406
|
+
}
|
|
4330
4407
|
} else if (type === "path") {
|
|
4331
4408
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4409
|
+
} else if (type === "clipping") {
|
|
4410
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4411
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4412
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4413
|
+
[[i, 1]],
|
|
4414
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4415
|
+
]).flat();
|
|
4416
|
+
let geometry;
|
|
4417
|
+
if (data.weights) {
|
|
4418
|
+
const starts = [];
|
|
4419
|
+
let offset = 0;
|
|
4420
|
+
for (const list of data.weights) {
|
|
4421
|
+
starts.push(offset);
|
|
4422
|
+
offset += list.length * 2;
|
|
4423
|
+
}
|
|
4424
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4425
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4426
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4427
|
+
} else {
|
|
4428
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4429
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4430
|
+
clipDeforms.set(id, expand);
|
|
4431
|
+
}
|
|
4432
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4332
4433
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4333
4434
|
}
|
|
4334
4435
|
}
|
|
4335
4436
|
}
|
|
4336
|
-
const
|
|
4437
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s) => s[kind]?.includes(c.name));
|
|
4438
|
+
const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4337
4439
|
id: ikIds.get(c.name),
|
|
4338
4440
|
order: c.order ?? 0,
|
|
4339
4441
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4346,7 +4448,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4346
4448
|
stretch: !!c.stretch,
|
|
4347
4449
|
uniform: !!c.uniform
|
|
4348
4450
|
}));
|
|
4349
|
-
const transform = (json.transform ?? []).map((c) => ({
|
|
4451
|
+
const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4350
4452
|
id: tcIds.get(c.name),
|
|
4351
4453
|
order: c.order ?? 0,
|
|
4352
4454
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4366,7 +4468,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4366
4468
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4367
4469
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4368
4470
|
}));
|
|
4369
|
-
const path = (json.path ?? []).map((c) => ({
|
|
4471
|
+
const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4370
4472
|
id: pcIds.get(c.name),
|
|
4371
4473
|
order: c.order ?? 0,
|
|
4372
4474
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4401,7 +4503,21 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4401
4503
|
for (let i = 0; i < frames.length; i++) {
|
|
4402
4504
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4403
4505
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4404
|
-
|
|
4506
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4507
|
+
const value = f[field] ?? def;
|
|
4508
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4509
|
+
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;
|
|
4510
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4511
|
+
const u = (k + 1) / 10;
|
|
4512
|
+
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" };
|
|
4513
|
+
});
|
|
4514
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4515
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4519
|
+
}
|
|
4520
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4405
4521
|
}
|
|
4406
4522
|
return keys;
|
|
4407
4523
|
};
|
|
@@ -4412,6 +4528,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4412
4528
|
const seen = (frames) => {
|
|
4413
4529
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4414
4530
|
};
|
|
4531
|
+
if (a.physics) {
|
|
4532
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4533
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4534
|
+
}
|
|
4415
4535
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4416
4536
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4417
4537
|
if (!sb) {
|
|
@@ -4452,19 +4572,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4452
4572
|
if (kind === "attachment") {
|
|
4453
4573
|
attachmentFrames.set(slotName, frames);
|
|
4454
4574
|
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 (
|
|
4575
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4576
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4577
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4456
4578
|
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);
|
|
4579
|
+
const c = hex8(f.color ?? f.light);
|
|
4580
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4459
4581
|
};
|
|
4582
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4460
4583
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4461
4584
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4462
|
-
return {
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4585
|
+
return {
|
|
4586
|
+
t: r4(f.time ?? 0),
|
|
4587
|
+
v: rgba.map(r4),
|
|
4588
|
+
ease: "linear",
|
|
4589
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4590
|
+
};
|
|
4468
4591
|
})));
|
|
4469
4592
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4470
4593
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4503,15 +4626,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4503
4626
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4504
4627
|
continue;
|
|
4505
4628
|
}
|
|
4506
|
-
const
|
|
4629
|
+
const rawClip = skins.find((s) => s.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4630
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4631
|
+
const data = clipData ?? att;
|
|
4632
|
+
const length = data.weights ? 2 * data.weights.reduce((s, l) => s + l.length, 0) : data.vertices?.length ?? 0;
|
|
4507
4633
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4508
4634
|
const full = new Array(length).fill(0);
|
|
4509
4635
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4510
4636
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4511
4637
|
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" };
|
|
4638
|
+
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
4639
|
})));
|
|
4514
4640
|
}
|
|
4641
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4642
|
+
let changed = false;
|
|
4643
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4644
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4645
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4646
|
+
if (source) {
|
|
4647
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4648
|
+
changed = true;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
if (!changed) break;
|
|
4652
|
+
}
|
|
4515
4653
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4516
4654
|
if (Array.isArray(drawOrderFrames)) {
|
|
4517
4655
|
seen(drawOrderFrames);
|
|
@@ -4534,20 +4672,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4534
4672
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4535
4673
|
seen(frames);
|
|
4536
4674
|
const id = ikIds.get(name);
|
|
4675
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4537
4676
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4538
4677
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4539
4678
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4540
4679
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4541
4680
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4542
4681
|
}
|
|
4543
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4682
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4683
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4684
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4685
|
+
t: r4(f.time ?? 0),
|
|
4686
|
+
v: r4(value(f)),
|
|
4687
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4688
|
+
})));
|
|
4689
|
+
};
|
|
4548
4690
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4549
4691
|
seen(frames);
|
|
4550
4692
|
const id = tcIds.get(name);
|
|
4693
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4551
4694
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4552
4695
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4553
4696
|
}
|
|
@@ -4555,6 +4698,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4555
4698
|
const id = pcIds.get(name);
|
|
4556
4699
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4557
4700
|
seen(frames);
|
|
4701
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4558
4702
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4559
4703
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4560
4704
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4717,21 +4861,28 @@ async function importSpineProject(opts) {
|
|
|
4717
4861
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4718
4862
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4719
4863
|
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");
|
|
4864
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4865
|
+
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
4866
|
const rigDir = dirname6(opts.rigPath);
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4867
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4868
|
+
for (const page2 of pages) {
|
|
4869
|
+
if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
|
|
4870
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4871
|
+
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}`);
|
|
4872
|
+
let pngPath = imagePath;
|
|
4873
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4874
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4875
|
+
if (!await exists(pngPath)) {
|
|
4876
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4877
|
+
await writeFile5(pngPath, png);
|
|
4878
|
+
}
|
|
4730
4879
|
}
|
|
4880
|
+
pagePaths.set(page2.name, pngPath);
|
|
4731
4881
|
}
|
|
4732
|
-
const pageImage =
|
|
4882
|
+
const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4733
4883
|
const name = opts.name ?? basename4(stem);
|
|
4734
|
-
|
|
4884
|
+
const imagePaths = [...pagePaths.values()];
|
|
4885
|
+
return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4735
4886
|
}
|
|
4736
4887
|
|
|
4737
4888
|
// src/rig-templates.ts
|
|
@@ -5264,13 +5415,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5264
5415
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5265
5416
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5266
5417
|
const id = `attach_${bone}_${target}`;
|
|
5418
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5267
5419
|
const offset = {
|
|
5268
5420
|
x,
|
|
5269
5421
|
y,
|
|
5270
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5422
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5271
5423
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5272
5424
|
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)
|
|
5425
|
+
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
5426
|
};
|
|
5275
5427
|
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
5428
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5420,8 +5572,8 @@ async function load(path) {
|
|
|
5420
5572
|
}
|
|
5421
5573
|
var save = (path, doc) => saveJson(path, doc);
|
|
5422
5574
|
var exists2 = (p) => stat2(p).then(() => true, () => false);
|
|
5423
|
-
var resolveFrom = (rigPath, p) =>
|
|
5424
|
-
var srcFor = (rigPath, file) =>
|
|
5575
|
+
var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
|
|
5576
|
+
var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
|
|
5425
5577
|
var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
|
|
5426
5578
|
async function pngInfo(rigPath, file) {
|
|
5427
5579
|
const abs = resolveFrom(rigPath, file);
|
|
@@ -5533,10 +5685,11 @@ tool(
|
|
|
5533
5685
|
var KeyIn = z3.object({
|
|
5534
5686
|
t: z3.number().describe("seconds"),
|
|
5535
5687
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5688
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5536
5689
|
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
5690
|
});
|
|
5538
5691
|
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].";
|
|
5692
|
+
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
5693
|
tool(
|
|
5541
5694
|
"rig_create",
|
|
5542
5695
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5660,19 +5813,20 @@ tool(
|
|
|
5660
5813
|
);
|
|
5661
5814
|
tool(
|
|
5662
5815
|
"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;
|
|
5816
|
+
"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
5817
|
{
|
|
5665
5818
|
path: Path,
|
|
5666
5819
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5667
5820
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5668
5821
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5822
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5669
5823
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5670
5824
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5671
5825
|
notes: z3.string().optional()
|
|
5672
5826
|
},
|
|
5673
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5827
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5674
5828
|
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 });
|
|
5829
|
+
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
5830
|
const res = await create(path, parse(r.doc));
|
|
5677
5831
|
if (r.warnings.length) res.text += `
|
|
5678
5832
|
warnings:
|
|
@@ -6443,9 +6597,39 @@ tool(
|
|
|
6443
6597
|
}
|
|
6444
6598
|
}
|
|
6445
6599
|
);
|
|
6600
|
+
|
|
6601
|
+
// src/rig-version.ts
|
|
6602
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
6603
|
+
async function rigVersion(abs) {
|
|
6604
|
+
try {
|
|
6605
|
+
const s = await stat3(abs);
|
|
6606
|
+
return `${s.mtimeMs}:${s.size}`;
|
|
6607
|
+
} catch {
|
|
6608
|
+
return "missing";
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
async function callToolVersioned(name, args) {
|
|
6612
|
+
const path = typeof args.path === "string" ? args.path : void 0;
|
|
6613
|
+
if (!path) return { result: await callTool(name, args), before: "missing", after: "missing" };
|
|
6614
|
+
return withRigQueue(path, async () => {
|
|
6615
|
+
const before = await rigVersion(path);
|
|
6616
|
+
const result = await callTool(name, args);
|
|
6617
|
+
return { result, before, after: await rigVersion(path) };
|
|
6618
|
+
});
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
// src/tool-schema.ts
|
|
6622
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
6623
|
+
function jsonSchemaOf(tool2) {
|
|
6624
|
+
const { $schema: _drop, ...schema } = zodToJsonSchema(tool2.input, { $refStrategy: "none" });
|
|
6625
|
+
return schema;
|
|
6626
|
+
}
|
|
6446
6627
|
export {
|
|
6447
6628
|
callTool,
|
|
6629
|
+
callToolVersioned,
|
|
6448
6630
|
closeTools,
|
|
6631
|
+
jsonSchemaOf,
|
|
6632
|
+
rigVersion,
|
|
6449
6633
|
tools
|
|
6450
6634
|
};
|
|
6451
6635
|
//# sourceMappingURL=tools.js.map
|