@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/cli.js
CHANGED
|
@@ -3,9 +3,9 @@ import { resolve as resolve3 } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
// src/editor/server.ts
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { readFile as readFile10, stat as
|
|
6
|
+
import { readFile as readFile10, stat as stat4, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
7
7
|
import { watch as fsWatch } from "node:fs";
|
|
8
|
-
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as
|
|
8
|
+
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep as sep2, join as join10, relative as relative4 } from "node:path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
10
10
|
|
|
11
11
|
// src/rig-format.ts
|
|
@@ -176,7 +176,9 @@ var PathAttachment = z.object({
|
|
|
176
176
|
/** curve lengths for constant-speed positioning */
|
|
177
177
|
lengths: z.array(z.number()).default([]),
|
|
178
178
|
closed: z.boolean().default(false),
|
|
179
|
-
constantSpeed: z.boolean().default(true)
|
|
179
|
+
constantSpeed: z.boolean().default(true),
|
|
180
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
181
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
180
182
|
});
|
|
181
183
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
182
184
|
var IkConstraint = z.object({
|
|
@@ -245,7 +247,9 @@ var Key = z.object({
|
|
|
245
247
|
t: z.number().nonnegative(),
|
|
246
248
|
v: KeyValue,
|
|
247
249
|
/** easing INTO the next key */
|
|
248
|
-
ease: Ease.default("ease_in_out")
|
|
250
|
+
ease: Ease.default("ease_in_out"),
|
|
251
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
252
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
249
253
|
});
|
|
250
254
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
251
255
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -432,6 +436,10 @@ function validate(doc) {
|
|
|
432
436
|
else if (a.slot !== s2.id) errs.push(`slot "${s2.id}": attachment "${s2.attachment}" belongs to slot "${a.slot}"`);
|
|
433
437
|
}
|
|
434
438
|
}
|
|
439
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
440
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
441
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
442
|
+
}
|
|
435
443
|
const vertexCount2 = (a) => {
|
|
436
444
|
if (a.type === "region") return 4;
|
|
437
445
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -515,6 +523,7 @@ function validate(doc) {
|
|
|
515
523
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
516
524
|
let prev = -1;
|
|
517
525
|
for (const k of t.keys) {
|
|
526
|
+
if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
|
|
518
527
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
519
528
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
520
529
|
if (t.target === "control") {
|
|
@@ -562,7 +571,7 @@ function parse(input) {
|
|
|
562
571
|
// src/rig-api.ts
|
|
563
572
|
import { z as z3 } from "zod";
|
|
564
573
|
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
565
|
-
import { dirname as dirname8, join as join9, relative as
|
|
574
|
+
import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
|
|
566
575
|
|
|
567
576
|
// src/rig-io.ts
|
|
568
577
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -585,8 +594,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
|
|
|
585
594
|
// src/rig-queue.ts
|
|
586
595
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
587
596
|
import { resolve } from "node:path";
|
|
588
|
-
var pending
|
|
589
|
-
|
|
597
|
+
var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
|
|
598
|
+
pending: /* @__PURE__ */ new Map(),
|
|
599
|
+
active: new AsyncLocalStorage()
|
|
600
|
+
};
|
|
590
601
|
async function withRigQueue(path, op) {
|
|
591
602
|
const key = resolve(path);
|
|
592
603
|
const held = active.getStore();
|
|
@@ -742,8 +753,8 @@ function sample(track, t, loop = false) {
|
|
|
742
753
|
}
|
|
743
754
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
744
755
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
745
|
-
const
|
|
746
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
756
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
757
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
747
758
|
}
|
|
748
759
|
return a.v;
|
|
749
760
|
}
|
|
@@ -835,6 +846,13 @@ function boneWorldOf(bone, s2, parent) {
|
|
|
835
846
|
if (!parent) return local;
|
|
836
847
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
837
848
|
const [tx, ty] = apply(parent, s2.x, s2.y);
|
|
849
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
850
|
+
const r = s2.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
|
|
851
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
852
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
853
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
854
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s2.scaleX, s2.scaleY, s2.shearX, s2.shearY)), tx, ty };
|
|
855
|
+
}
|
|
838
856
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
839
857
|
}
|
|
840
858
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -867,11 +885,11 @@ var localOf = (doc, pose, b) => {
|
|
|
867
885
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
868
886
|
};
|
|
869
887
|
};
|
|
870
|
-
function updateSubtree(skel, root3) {
|
|
888
|
+
function updateSubtree(skel, root3, includeRoot = true) {
|
|
871
889
|
const doc = skel.doc;
|
|
872
890
|
const kids = /* @__PURE__ */ new Map();
|
|
873
891
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
874
|
-
const stack = [root3];
|
|
892
|
+
const stack = includeRoot ? [root3] : (kids.get(root3) ?? []).map((b) => b.id);
|
|
875
893
|
while (stack.length) {
|
|
876
894
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
877
895
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1114,7 +1132,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1114
1132
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1115
1133
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1116
1134
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1117
|
-
const
|
|
1135
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1136
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1118
1137
|
for (const id of c.bones) {
|
|
1119
1138
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1120
1139
|
if (c.local) {
|
|
@@ -1276,6 +1295,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1276
1295
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1277
1296
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1278
1297
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1298
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1279
1299
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1280
1300
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1281
1301
|
let sum = 0;
|
|
@@ -1310,9 +1330,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1310
1330
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1311
1331
|
}
|
|
1312
1332
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1333
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1313
1334
|
for (let i = 0; i < bones.length; i++) {
|
|
1314
1335
|
const { b, st } = bones[i];
|
|
1315
|
-
let w = { ...
|
|
1336
|
+
let w = { ...inputWorlds[i] };
|
|
1316
1337
|
w.tx += (boneX - w.tx) * mixX;
|
|
1317
1338
|
w.ty += (boneY - w.ty) * mixY;
|
|
1318
1339
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1342,7 +1363,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1342
1363
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1343
1364
|
w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
|
|
1344
1365
|
}
|
|
1345
|
-
|
|
1366
|
+
solvedWorlds.set(b.id, w);
|
|
1367
|
+
}
|
|
1368
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1369
|
+
const world = solvedWorlds.get(b.id);
|
|
1370
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1346
1371
|
}
|
|
1347
1372
|
}
|
|
1348
1373
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1371,9 +1396,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1371
1396
|
case "onlyTranslation":
|
|
1372
1397
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1373
1398
|
case "noRotationOrReflection": {
|
|
1374
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1399
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1375
1400
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1376
|
-
|
|
1401
|
+
if (lenX * lenX <= 1e-4) {
|
|
1402
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1403
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1404
|
+
}
|
|
1405
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1406
|
+
return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
|
|
1377
1407
|
}
|
|
1378
1408
|
case "noScale":
|
|
1379
1409
|
case "noScaleOrReflection": {
|
|
@@ -1399,7 +1429,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1399
1429
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1400
1430
|
st.shearX = 0;
|
|
1401
1431
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1402
|
-
|
|
1432
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1433
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1434
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1435
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1436
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1437
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1438
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1439
|
+
}
|
|
1440
|
+
st.world = world;
|
|
1441
|
+
updateSubtree(skel, bone.id, false);
|
|
1403
1442
|
}
|
|
1404
1443
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1405
1444
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1974,8 +2013,9 @@ function createDoc(name, width, height, notes) {
|
|
|
1974
2013
|
function createSymbol(name, asset, margin = 0.15, notes) {
|
|
1975
2014
|
const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
|
|
1976
2015
|
const width = asset.width + 2 * mx, height = asset.height + 2 * my;
|
|
1977
|
-
const
|
|
1978
|
-
const
|
|
2016
|
+
const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
2017
|
+
const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
|
|
2018
|
+
const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
|
|
1979
2019
|
let doc = createDoc(name, width, height, notes);
|
|
1980
2020
|
doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
|
|
1981
2021
|
doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
|
|
@@ -2158,7 +2198,7 @@ function removeSlot(doc, id) {
|
|
|
2158
2198
|
return patchDrawOrder({
|
|
2159
2199
|
...doc,
|
|
2160
2200
|
slots: doc.slots.filter((s2) => s2.id !== id).map((s2) => gone.has(s2.clip ?? "") ? { ...s2, clip: void 0 } : s2),
|
|
2161
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2201
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2162
2202
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2163
2203
|
}, (o) => o.filter((s2) => s2 !== id));
|
|
2164
2204
|
}
|
|
@@ -2554,7 +2594,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2554
2594
|
return {
|
|
2555
2595
|
...doc,
|
|
2556
2596
|
slots: doc.slots.map((s2) => ({ ...s2, id: sub(s2.id) })),
|
|
2557
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2597
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2558
2598
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2559
2599
|
};
|
|
2560
2600
|
}
|
|
@@ -2731,7 +2771,7 @@ function setGoal(doc, bone, x, y, opts) {
|
|
|
2731
2771
|
|
|
2732
2772
|
// src/rig-import-layers.ts
|
|
2733
2773
|
import { readdir, readFile } from "node:fs/promises";
|
|
2734
|
-
import { join as join2, basename as basename2 } from "node:path";
|
|
2774
|
+
import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
|
|
2735
2775
|
var DEFAULT_PARENT = {
|
|
2736
2776
|
head: "neck",
|
|
2737
2777
|
neck: "torso",
|
|
@@ -2757,6 +2797,10 @@ function pngSize(buf) {
|
|
|
2757
2797
|
if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
|
|
2758
2798
|
return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
|
|
2759
2799
|
}
|
|
2800
|
+
function outsideDir(dir, file) {
|
|
2801
|
+
const r = relative(dir, join2(dir, file));
|
|
2802
|
+
return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
|
|
2803
|
+
}
|
|
2760
2804
|
async function importLayers(doc, dir, relPrefix = "layers") {
|
|
2761
2805
|
let specs;
|
|
2762
2806
|
try {
|
|
@@ -2769,6 +2813,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
|
|
|
2769
2813
|
let next = doc;
|
|
2770
2814
|
const parsed = [];
|
|
2771
2815
|
for (const s2 of specs) {
|
|
2816
|
+
if (outsideDir(dir, s2.file)) throw new Error(`layers.json file "${s2.file}" is outside the layers folder (${dir})`);
|
|
2772
2817
|
const buf = await readFile(join2(dir, s2.file));
|
|
2773
2818
|
const [w, h] = pngSize(buf);
|
|
2774
2819
|
const m = basename2(s2.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
|
|
@@ -4025,7 +4070,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
|
|
|
4025
4070
|
|
|
4026
4071
|
// src/spine-import.ts
|
|
4027
4072
|
import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
|
|
4028
|
-
import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
|
|
4073
|
+
import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
|
|
4029
4074
|
|
|
4030
4075
|
// src/ktx2.ts
|
|
4031
4076
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
@@ -4167,21 +4212,26 @@ var Ids = class {
|
|
|
4167
4212
|
has(name) {
|
|
4168
4213
|
return this.map.has(name);
|
|
4169
4214
|
}
|
|
4215
|
+
reserve(id) {
|
|
4216
|
+
this.used.add(id);
|
|
4217
|
+
}
|
|
4170
4218
|
};
|
|
4171
4219
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4172
4220
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
4173
4221
|
var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
|
|
4174
|
-
function importSpine(
|
|
4222
|
+
function importSpine(json2, atlasText, opts = {}) {
|
|
4175
4223
|
const warnings = [];
|
|
4176
4224
|
const atlas = parseAtlas(atlasText);
|
|
4177
|
-
const version = String(
|
|
4225
|
+
const version = String(json2.skeleton?.spine ?? "4.0");
|
|
4226
|
+
if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4178
4227
|
const major = parseFloat(version);
|
|
4179
|
-
const bounds = { x:
|
|
4228
|
+
const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
|
|
4180
4229
|
const margin = opts.margin ?? 0;
|
|
4181
4230
|
let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
|
|
4182
4231
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4183
4232
|
const boneIds = new Ids("b_"), slotIds = new Ids("s_"), assetIds = new Ids("a_"), attIds = new Ids("att_"), ikIds = new Ids("ik_"), tcIds = new Ids("tc_"), pcIds = new Ids("pc_"), animIds = new Ids("anim_");
|
|
4184
4233
|
const ROOT = "origin";
|
|
4234
|
+
boneIds.reserve(ROOT);
|
|
4185
4235
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4186
4236
|
const attachmentId = (slot, name) => {
|
|
4187
4237
|
const key = `${slot} ${name}`;
|
|
@@ -4192,7 +4242,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4192
4242
|
}
|
|
4193
4243
|
return id;
|
|
4194
4244
|
};
|
|
4195
|
-
const spineBones =
|
|
4245
|
+
const spineBones = json2.bones ?? [];
|
|
4196
4246
|
const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
|
|
4197
4247
|
const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
|
|
4198
4248
|
for (const b of spineBones) {
|
|
@@ -4205,14 +4255,18 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4205
4255
|
...b.color ? { color: b.color } : {}
|
|
4206
4256
|
});
|
|
4207
4257
|
}
|
|
4208
|
-
const skins = Array.isArray(
|
|
4258
|
+
const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4259
|
+
const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
|
|
4260
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
|
|
4261
|
+
const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
|
|
4262
|
+
if (skins.length > 1) warnings.push(`Spine skin "${selectedSkin?.name}" selected; setup and attachment timelines are flattened to this skin with default fallback (no runtime skin switching)`);
|
|
4209
4263
|
const frameSuffix = (seq, index) => {
|
|
4210
|
-
const n = String((seq.start ??
|
|
4264
|
+
const n = String((seq.start ?? 1) + index);
|
|
4211
4265
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4212
4266
|
};
|
|
4213
4267
|
const sequences = /* @__PURE__ */ new Map();
|
|
4214
4268
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4215
|
-
for (const skin2 of
|
|
4269
|
+
for (const skin2 of lookupSkins) {
|
|
4216
4270
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4217
4271
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4218
4272
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4221,19 +4275,23 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4221
4275
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4222
4276
|
}
|
|
4223
4277
|
}
|
|
4224
|
-
const attachmentRef = (slot, name) =>
|
|
4225
|
-
|
|
4278
|
+
const attachmentRef = (slot, name) => {
|
|
4279
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4280
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4281
|
+
};
|
|
4282
|
+
const spineSlots = json2.slots ?? [];
|
|
4226
4283
|
const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
|
|
4227
4284
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
4228
4285
|
const slots = spineSlots.map((s2, i) => ({
|
|
4229
4286
|
id: slotIds.get(s2.name),
|
|
4230
4287
|
bone: boneIds.get(s2.bone),
|
|
4231
4288
|
z: i,
|
|
4232
|
-
...s2.attachment ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4289
|
+
...s2.attachment && attachmentRef(s2.name, s2.attachment) ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4233
4290
|
blend: blendOf(s2.blend),
|
|
4234
4291
|
color: hex8(s2.color),
|
|
4235
4292
|
alpha: 1
|
|
4236
4293
|
}));
|
|
4294
|
+
if (spineSlots.some((s2) => s2.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4237
4295
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4238
4296
|
const assets = /* @__PURE__ */ new Map();
|
|
4239
4297
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4259,6 +4317,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4259
4317
|
return id;
|
|
4260
4318
|
};
|
|
4261
4319
|
const attachments = [];
|
|
4320
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4262
4321
|
const readVertices = (a, vertexCount2) => {
|
|
4263
4322
|
const v = a.vertices ?? [];
|
|
4264
4323
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4279,6 +4338,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4279
4338
|
}
|
|
4280
4339
|
return { weights };
|
|
4281
4340
|
};
|
|
4341
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4342
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4343
|
+
if (visiting.has(key)) {
|
|
4344
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4345
|
+
return void 0;
|
|
4346
|
+
}
|
|
4347
|
+
const raw = skins.find((s2) => s2.name === skinName)?.attachments?.[slot]?.[name];
|
|
4348
|
+
if (!raw) {
|
|
4349
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4350
|
+
return void 0;
|
|
4351
|
+
}
|
|
4352
|
+
if (!raw.parent) return raw;
|
|
4353
|
+
visiting.add(key);
|
|
4354
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4355
|
+
};
|
|
4356
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4282
4357
|
for (const skin2 of skins) {
|
|
4283
4358
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4284
4359
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4290,7 +4365,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4290
4365
|
for (const [name, a] of Object.entries(atts)) {
|
|
4291
4366
|
if (a.sequence) {
|
|
4292
4367
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4293
|
-
const base = a.path ?? name;
|
|
4368
|
+
const base = a.path ?? a.name ?? name;
|
|
4294
4369
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4295
4370
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4296
4371
|
if (!asset) continue;
|
|
@@ -4313,7 +4388,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4313
4388
|
}
|
|
4314
4389
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4315
4390
|
if (type === "region") {
|
|
4316
|
-
const asset = assetFor(a.path ?? name);
|
|
4391
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4317
4392
|
if (!asset) continue;
|
|
4318
4393
|
attachments.push({
|
|
4319
4394
|
id,
|
|
@@ -4329,21 +4404,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4329
4404
|
...a.height ? { height: a.height } : {}
|
|
4330
4405
|
});
|
|
4331
4406
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
}
|
|
4336
|
-
const asset = assetFor(a.path ?? name);
|
|
4407
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4408
|
+
if (!geometry) continue;
|
|
4409
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4337
4410
|
if (!asset) continue;
|
|
4338
|
-
const n = (
|
|
4339
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4411
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4412
|
+
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 } : {} });
|
|
4413
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4414
|
+
const parentSkin = a.skin ?? "default";
|
|
4415
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4416
|
+
}
|
|
4340
4417
|
} else if (type === "path") {
|
|
4341
4418
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4419
|
+
} else if (type === "clipping") {
|
|
4420
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4421
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4422
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4423
|
+
[[i, 1]],
|
|
4424
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4425
|
+
]).flat();
|
|
4426
|
+
let geometry;
|
|
4427
|
+
if (data.weights) {
|
|
4428
|
+
const starts = [];
|
|
4429
|
+
let offset = 0;
|
|
4430
|
+
for (const list of data.weights) {
|
|
4431
|
+
starts.push(offset);
|
|
4432
|
+
offset += list.length * 2;
|
|
4433
|
+
}
|
|
4434
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4435
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4436
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4437
|
+
} else {
|
|
4438
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4439
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4440
|
+
clipDeforms.set(id, expand);
|
|
4441
|
+
}
|
|
4442
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4342
4443
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4343
4444
|
}
|
|
4344
4445
|
}
|
|
4345
4446
|
}
|
|
4346
|
-
const
|
|
4447
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
|
|
4448
|
+
const ik = (json2.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4347
4449
|
id: ikIds.get(c.name),
|
|
4348
4450
|
order: c.order ?? 0,
|
|
4349
4451
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4356,7 +4458,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4356
4458
|
stretch: !!c.stretch,
|
|
4357
4459
|
uniform: !!c.uniform
|
|
4358
4460
|
}));
|
|
4359
|
-
const transform = (
|
|
4461
|
+
const transform = (json2.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4360
4462
|
id: tcIds.get(c.name),
|
|
4361
4463
|
order: c.order ?? 0,
|
|
4362
4464
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4376,7 +4478,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4376
4478
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4377
4479
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4378
4480
|
}));
|
|
4379
|
-
const path = (
|
|
4481
|
+
const path = (json2.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4380
4482
|
id: pcIds.get(c.name),
|
|
4381
4483
|
order: c.order ?? 0,
|
|
4382
4484
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4411,17 +4513,35 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4411
4513
|
for (let i = 0; i < frames.length; i++) {
|
|
4412
4514
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4413
4515
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4414
|
-
|
|
4516
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4517
|
+
const value = f[field] ?? def;
|
|
4518
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4519
|
+
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;
|
|
4520
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4521
|
+
const u = (k + 1) / 10;
|
|
4522
|
+
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" };
|
|
4523
|
+
});
|
|
4524
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4525
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4526
|
+
continue;
|
|
4527
|
+
}
|
|
4528
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4529
|
+
}
|
|
4530
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4415
4531
|
}
|
|
4416
4532
|
return keys;
|
|
4417
4533
|
};
|
|
4418
4534
|
const animations = [];
|
|
4419
|
-
for (const [animName, a] of Object.entries(
|
|
4535
|
+
for (const [animName, a] of Object.entries(json2.animations ?? {})) {
|
|
4420
4536
|
const tracks = [], events = [];
|
|
4421
4537
|
let duration = 0;
|
|
4422
4538
|
const seen = (frames) => {
|
|
4423
4539
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4424
4540
|
};
|
|
4541
|
+
if (a.physics) {
|
|
4542
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4543
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4544
|
+
}
|
|
4425
4545
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4426
4546
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4427
4547
|
if (!sb) {
|
|
@@ -4462,19 +4582,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4462
4582
|
if (kind === "attachment") {
|
|
4463
4583
|
attachmentFrames.set(slotName, frames);
|
|
4464
4584
|
tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
|
|
4465
|
-
} else if (
|
|
4585
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4586
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4587
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4466
4588
|
const rgbaOf = (f) => {
|
|
4467
|
-
const c = hex8(f.color);
|
|
4468
|
-
return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4589
|
+
const c = hex8(f.color ?? f.light);
|
|
4590
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4469
4591
|
};
|
|
4592
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4470
4593
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4471
4594
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4472
|
-
return {
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4595
|
+
return {
|
|
4596
|
+
t: r4(f.time ?? 0),
|
|
4597
|
+
v: rgba.map(r4),
|
|
4598
|
+
ease: "linear",
|
|
4599
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4600
|
+
};
|
|
4478
4601
|
})));
|
|
4479
4602
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4480
4603
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4513,15 +4636,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4513
4636
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4514
4637
|
continue;
|
|
4515
4638
|
}
|
|
4516
|
-
const
|
|
4639
|
+
const rawClip = skins.find((s2) => s2.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4640
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4641
|
+
const data = clipData ?? att;
|
|
4642
|
+
const length = data.weights ? 2 * data.weights.reduce((s2, l) => s2 + l.length, 0) : data.vertices?.length ?? 0;
|
|
4517
4643
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4518
4644
|
const full = new Array(length).fill(0);
|
|
4519
4645
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4520
4646
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4521
4647
|
const next = frames[i + 1];
|
|
4522
|
-
return { t: r4(f.time ?? 0), v: full.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4648
|
+
return { t: r4(f.time ?? 0), v: (clipDeforms.get(id)?.(full) ?? full).map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4523
4649
|
})));
|
|
4524
4650
|
}
|
|
4651
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4652
|
+
let changed = false;
|
|
4653
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4654
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4655
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4656
|
+
if (source) {
|
|
4657
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4658
|
+
changed = true;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
if (!changed) break;
|
|
4662
|
+
}
|
|
4525
4663
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4526
4664
|
if (Array.isArray(drawOrderFrames)) {
|
|
4527
4665
|
seen(drawOrderFrames);
|
|
@@ -4544,20 +4682,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4544
4682
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4545
4683
|
seen(frames);
|
|
4546
4684
|
const id = ikIds.get(name);
|
|
4685
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4547
4686
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4548
4687
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4549
4688
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4550
4689
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4551
4690
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4552
4691
|
}
|
|
4553
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4692
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4693
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4694
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4695
|
+
t: r4(f.time ?? 0),
|
|
4696
|
+
v: r4(value(f)),
|
|
4697
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4698
|
+
})));
|
|
4699
|
+
};
|
|
4558
4700
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4559
4701
|
seen(frames);
|
|
4560
4702
|
const id = tcIds.get(name);
|
|
4703
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4561
4704
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4562
4705
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4563
4706
|
}
|
|
@@ -4565,6 +4708,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4565
4708
|
const id = pcIds.get(name);
|
|
4566
4709
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4567
4710
|
seen(frames);
|
|
4711
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4568
4712
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4569
4713
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4570
4714
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4669,7 +4813,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4669
4813
|
const build = () => ({
|
|
4670
4814
|
format: "energy8.rig",
|
|
4671
4815
|
version: 2,
|
|
4672
|
-
meta: { name: opts.name ??
|
|
4816
|
+
meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
|
|
4673
4817
|
assets: [...assets.values()],
|
|
4674
4818
|
bones,
|
|
4675
4819
|
slots,
|
|
@@ -4722,26 +4866,33 @@ async function firstExisting(candidates, what) {
|
|
|
4722
4866
|
throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
|
|
4723
4867
|
}
|
|
4724
4868
|
async function importSpineProject(opts) {
|
|
4725
|
-
const
|
|
4869
|
+
const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
|
|
4726
4870
|
const stem = opts.json.replace(/\.json$/i, "");
|
|
4727
4871
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4728
4872
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4729
4873
|
const { pages } = parseAtlas(atlasText);
|
|
4730
|
-
if (pages.length
|
|
4731
|
-
|
|
4732
|
-
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
|
|
4874
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4875
|
+
if (opts.image && pages.length !== 1) throw new Error("image override requires a single-page atlas; multi-page atlases resolve each named page beside the atlas");
|
|
4733
4876
|
const rigDir = dirname6(opts.rigPath);
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4877
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const page2 of pages) {
|
|
4879
|
+
if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
|
|
4880
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4881
|
+
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}`);
|
|
4882
|
+
let pngPath = imagePath;
|
|
4883
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4884
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4885
|
+
if (!await exists(pngPath)) {
|
|
4886
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4887
|
+
await writeFile5(pngPath, png);
|
|
4888
|
+
}
|
|
4740
4889
|
}
|
|
4890
|
+
pagePaths.set(page2.name, pngPath);
|
|
4741
4891
|
}
|
|
4742
|
-
const pageImage =
|
|
4892
|
+
const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4743
4893
|
const name = opts.name ?? basename4(stem);
|
|
4744
|
-
|
|
4894
|
+
const imagePaths = [...pagePaths.values()];
|
|
4895
|
+
return { ...importSpine(json2, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4745
4896
|
}
|
|
4746
4897
|
|
|
4747
4898
|
// src/rig-templates.ts
|
|
@@ -5274,13 +5425,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5274
5425
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5275
5426
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5276
5427
|
const id = `attach_${bone}_${target}`;
|
|
5428
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5277
5429
|
const offset = {
|
|
5278
5430
|
x,
|
|
5279
5431
|
y,
|
|
5280
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5432
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5281
5433
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5282
5434
|
scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
|
|
5283
|
-
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI)
|
|
5435
|
+
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI) * handedness
|
|
5284
5436
|
};
|
|
5285
5437
|
const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
|
|
5286
5438
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5430,8 +5582,8 @@ async function load(path) {
|
|
|
5430
5582
|
}
|
|
5431
5583
|
var save = (path, doc) => saveJson(path, doc);
|
|
5432
5584
|
var exists2 = (p) => stat2(p).then(() => true, () => false);
|
|
5433
|
-
var resolveFrom = (rigPath, p) =>
|
|
5434
|
-
var srcFor = (rigPath, file) =>
|
|
5585
|
+
var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
|
|
5586
|
+
var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
|
|
5435
5587
|
var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
|
|
5436
5588
|
async function pngInfo(rigPath, file) {
|
|
5437
5589
|
const abs = resolveFrom(rigPath, file);
|
|
@@ -5543,10 +5695,11 @@ tool(
|
|
|
5543
5695
|
var KeyIn = z3.object({
|
|
5544
5696
|
t: z3.number().describe("seconds"),
|
|
5545
5697
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5698
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5546
5699
|
ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
|
|
5547
5700
|
});
|
|
5548
5701
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5549
|
-
var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5702
|
+
var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5550
5703
|
tool(
|
|
5551
5704
|
"rig_create",
|
|
5552
5705
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5670,19 +5823,20 @@ tool(
|
|
|
5670
5823
|
);
|
|
5671
5824
|
tool(
|
|
5672
5825
|
"rig_import_spine",
|
|
5673
|
-
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text;
|
|
5826
|
+
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
|
|
5674
5827
|
{
|
|
5675
5828
|
path: Path,
|
|
5676
5829
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5677
5830
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5678
5831
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5832
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5679
5833
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5680
5834
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5681
5835
|
notes: z3.string().optional()
|
|
5682
5836
|
},
|
|
5683
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5837
|
+
async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5684
5838
|
try {
|
|
5685
|
-
const r = await importSpineProject({ json: resolveFrom(path,
|
|
5839
|
+
const r = await importSpineProject({ json: resolveFrom(path, json2), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
|
|
5686
5840
|
const res = await create(path, parse(r.doc));
|
|
5687
5841
|
if (r.warnings.length) res.text += `
|
|
5688
5842
|
warnings:
|
|
@@ -6454,40 +6608,41 @@ tool(
|
|
|
6454
6608
|
}
|
|
6455
6609
|
);
|
|
6456
6610
|
|
|
6611
|
+
// src/rig-version.ts
|
|
6612
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
6613
|
+
async function rigVersion(abs) {
|
|
6614
|
+
try {
|
|
6615
|
+
const s2 = await stat3(abs);
|
|
6616
|
+
return `${s2.mtimeMs}:${s2.size}`;
|
|
6617
|
+
} catch {
|
|
6618
|
+
return "missing";
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6457
6622
|
// src/editor/server.ts
|
|
6458
6623
|
var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".map": "application/json", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp" };
|
|
6459
|
-
|
|
6624
|
+
var json = (res, status, body) => {
|
|
6625
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
6626
|
+
res.end(JSON.stringify(body));
|
|
6627
|
+
};
|
|
6628
|
+
function createEditorHandler(opts) {
|
|
6460
6629
|
const root3 = resolve2(opts.root);
|
|
6461
6630
|
const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
|
|
6462
6631
|
const inside = (rel) => {
|
|
6463
6632
|
const abs = resolve2(root3, rel);
|
|
6464
|
-
return abs === root3 || abs.startsWith(root3 +
|
|
6465
|
-
};
|
|
6466
|
-
const version = async (abs) => {
|
|
6467
|
-
try {
|
|
6468
|
-
const s2 = await stat3(abs);
|
|
6469
|
-
return `${s2.mtimeMs}:${s2.size}`;
|
|
6470
|
-
} catch {
|
|
6471
|
-
return "missing";
|
|
6472
|
-
}
|
|
6633
|
+
return abs === root3 || abs.startsWith(root3 + sep2) ? abs : void 0;
|
|
6473
6634
|
};
|
|
6474
6635
|
const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
|
|
6475
6636
|
const readDocAt = async (abs) => {
|
|
6476
|
-
let v = await
|
|
6637
|
+
let v = await rigVersion(abs);
|
|
6477
6638
|
for (let i = 0; i < 3; i++) {
|
|
6478
6639
|
const doc = await readDoc(abs);
|
|
6479
|
-
const after = await
|
|
6640
|
+
const after = await rigVersion(abs);
|
|
6480
6641
|
if (after === v) return { doc, version: v };
|
|
6481
6642
|
v = after;
|
|
6482
6643
|
}
|
|
6483
6644
|
return { doc: await readDoc(abs), version: v };
|
|
6484
6645
|
};
|
|
6485
|
-
const json = (res, status, body2) => {
|
|
6486
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
6487
|
-
res.end(JSON.stringify(body2));
|
|
6488
|
-
};
|
|
6489
|
-
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6490
|
-
const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
|
|
6491
6646
|
const clients = /* @__PURE__ */ new Map();
|
|
6492
6647
|
const watchers = /* @__PURE__ */ new Map();
|
|
6493
6648
|
function ensureWatch(abs) {
|
|
@@ -6499,7 +6654,7 @@ async function startEditorServer(opts) {
|
|
|
6499
6654
|
const schedule = () => {
|
|
6500
6655
|
clearTimeout(entry.timer);
|
|
6501
6656
|
entry.timer = setTimeout(async () => {
|
|
6502
|
-
const v = await
|
|
6657
|
+
const v = await rigVersion(abs);
|
|
6503
6658
|
if (v === entry.last) return;
|
|
6504
6659
|
entry.last = v;
|
|
6505
6660
|
for (const res of clients.get(abs) ?? []) res.write(`event: changed
|
|
@@ -6508,7 +6663,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6508
6663
|
`);
|
|
6509
6664
|
}, 50);
|
|
6510
6665
|
};
|
|
6511
|
-
|
|
6666
|
+
rigVersion(abs).then((v) => {
|
|
6512
6667
|
entry.last = v;
|
|
6513
6668
|
});
|
|
6514
6669
|
watchers.set(abs, entry);
|
|
@@ -6534,7 +6689,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6534
6689
|
name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
|
|
6535
6690
|
} catch {
|
|
6536
6691
|
}
|
|
6537
|
-
out.push({ rig:
|
|
6692
|
+
out.push({ rig: relative4(root3, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
|
|
6538
6693
|
}
|
|
6539
6694
|
};
|
|
6540
6695
|
await walk(root3);
|
|
@@ -6568,20 +6723,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6568
6723
|
}
|
|
6569
6724
|
return Buffer.concat(chunks);
|
|
6570
6725
|
}
|
|
6571
|
-
|
|
6726
|
+
async function handle(req, res, rawUrl = req.url ?? "/") {
|
|
6572
6727
|
try {
|
|
6573
|
-
|
|
6574
|
-
const url = new URL(req.url ?? "/", "http://x");
|
|
6728
|
+
const url = new URL(rawUrl, "http://x");
|
|
6575
6729
|
if (url.pathname === "/") {
|
|
6576
|
-
res.writeHead(302, { location: "
|
|
6577
|
-
return res.end();
|
|
6730
|
+
res.writeHead(302, { location: "editor.html" + url.search });
|
|
6731
|
+
return void res.end();
|
|
6732
|
+
}
|
|
6733
|
+
if (url.pathname.startsWith("/api/")) {
|
|
6734
|
+
await api(url, req, res);
|
|
6735
|
+
return;
|
|
6578
6736
|
}
|
|
6579
|
-
if (url.pathname.startsWith("/api/")) return await api(url, req, res);
|
|
6580
6737
|
const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
|
|
6581
6738
|
const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
|
|
6582
6739
|
if (!abs) {
|
|
6583
6740
|
res.writeHead(403);
|
|
6584
|
-
return res.end("forbidden");
|
|
6741
|
+
return void res.end("forbidden");
|
|
6585
6742
|
}
|
|
6586
6743
|
try {
|
|
6587
6744
|
const data = await readFile10(abs);
|
|
@@ -6599,7 +6756,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6599
6756
|
}
|
|
6600
6757
|
json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
|
|
6601
6758
|
}
|
|
6602
|
-
}
|
|
6759
|
+
}
|
|
6603
6760
|
async function api(url, req, res) {
|
|
6604
6761
|
const rigOf = (rel) => {
|
|
6605
6762
|
if (!rel) return void 0;
|
|
@@ -6616,7 +6773,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6616
6773
|
res.writeHead(403);
|
|
6617
6774
|
return res.end();
|
|
6618
6775
|
}
|
|
6619
|
-
if (!await
|
|
6776
|
+
if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
|
|
6620
6777
|
ensureWatch(abs);
|
|
6621
6778
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
6622
6779
|
res.write(`: connected
|
|
@@ -6638,7 +6795,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6638
6795
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6639
6796
|
ensureWatch(abs);
|
|
6640
6797
|
return await withRigQueue(abs, async () => {
|
|
6641
|
-
if (baseVersion !== void 0 && await
|
|
6798
|
+
if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
|
|
6642
6799
|
const at = await readDocAt(abs);
|
|
6643
6800
|
return json(res, 409, {
|
|
6644
6801
|
isError: true,
|
|
@@ -6647,8 +6804,15 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6647
6804
|
text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
|
|
6648
6805
|
});
|
|
6649
6806
|
}
|
|
6650
|
-
const
|
|
6651
|
-
|
|
6807
|
+
const tool2 = url.pathname.slice(10);
|
|
6808
|
+
let given = { ...args2 ?? {}, path: abs };
|
|
6809
|
+
try {
|
|
6810
|
+
if (opts.args) given = opts.args(tool2, given);
|
|
6811
|
+
} catch (e) {
|
|
6812
|
+
return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
|
|
6813
|
+
}
|
|
6814
|
+
const r = await callTool(tool2, given);
|
|
6815
|
+
return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
|
|
6652
6816
|
});
|
|
6653
6817
|
}
|
|
6654
6818
|
if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
|
|
@@ -6659,11 +6823,11 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6659
6823
|
if (typeof rig !== "string" || !/(^|\/)rig\.json$/.test(rig)) return json(res, 400, { isError: true, text: "ERROR: a project is a directory with a rig.json \u2014 the path must end with /rig.json" });
|
|
6660
6824
|
const abs = inside(rig);
|
|
6661
6825
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6662
|
-
if (await
|
|
6826
|
+
if (await stat4(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
|
|
6663
6827
|
await mkdir7(dirname9(abs), { recursive: true });
|
|
6664
6828
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6665
6829
|
ensureWatch(abs);
|
|
6666
|
-
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await
|
|
6830
|
+
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
|
|
6667
6831
|
}
|
|
6668
6832
|
if (url.pathname === "/api/files" && req.method === "GET") {
|
|
6669
6833
|
const abs = rigOf(url.searchParams.get("rig"));
|
|
@@ -6677,8 +6841,8 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6677
6841
|
if (!/^[A-Za-z0-9._-]+\.png$/i.test(name) || name.startsWith(".")) return json(res, 400, { isError: true, text: `ERROR: bad file name "${name}" \u2014 letters, digits, . _ - and a .png extension` });
|
|
6678
6842
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
|
|
6679
6843
|
const target = join10(dirname9(abs), name);
|
|
6680
|
-
if (!inside(
|
|
6681
|
-
if (await
|
|
6844
|
+
if (!inside(relative4(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
|
|
6845
|
+
if (await stat4(target).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${name} already exists next to the rig \u2014 rename the file` });
|
|
6682
6846
|
const data = await bytes(req, UPLOAD_MAX);
|
|
6683
6847
|
if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
|
|
6684
6848
|
let size;
|
|
@@ -6692,6 +6856,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6692
6856
|
}
|
|
6693
6857
|
json(res, 404, { isError: true, text: "unknown api route" });
|
|
6694
6858
|
}
|
|
6859
|
+
return { handle, close() {
|
|
6860
|
+
for (const w of watchers.values()) {
|
|
6861
|
+
clearTimeout(w.timer);
|
|
6862
|
+
w.w.close();
|
|
6863
|
+
}
|
|
6864
|
+
for (const set of clients.values()) for (const c of set) c.end();
|
|
6865
|
+
} };
|
|
6866
|
+
}
|
|
6867
|
+
async function startEditorServer(opts) {
|
|
6868
|
+
const editor = createEditorHandler(opts);
|
|
6869
|
+
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6870
|
+
const server = createServer((req, res) => {
|
|
6871
|
+
if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
|
|
6872
|
+
return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
|
|
6873
|
+
void editor.handle(req, res);
|
|
6874
|
+
});
|
|
6695
6875
|
await new Promise((ok, fail2) => {
|
|
6696
6876
|
server.once("error", fail2);
|
|
6697
6877
|
server.listen(opts.port ?? 8765, "127.0.0.1", () => {
|
|
@@ -6702,11 +6882,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6702
6882
|
const port2 = server.address().port;
|
|
6703
6883
|
allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port2}`, `localhost:${port2}`, `[::1]:${port2}`]);
|
|
6704
6884
|
return { port: port2, url: `http://127.0.0.1:${port2}`, close: async () => {
|
|
6705
|
-
|
|
6706
|
-
clearTimeout(w.timer);
|
|
6707
|
-
w.w.close();
|
|
6708
|
-
}
|
|
6709
|
-
for (const set of clients.values()) for (const c of set) c.end();
|
|
6885
|
+
editor.close();
|
|
6710
6886
|
await new Promise((r) => server.close(() => r()));
|
|
6711
6887
|
} };
|
|
6712
6888
|
}
|