@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/editor-entry.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/editor/server.ts
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { readFile as readFile10, stat as
|
|
3
|
+
import { readFile as readFile10, stat as stat4, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
4
4
|
import { watch as fsWatch } from "node:fs";
|
|
5
|
-
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as
|
|
5
|
+
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";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/rig-format.ts
|
|
@@ -173,7 +173,9 @@ var PathAttachment = z.object({
|
|
|
173
173
|
/** curve lengths for constant-speed positioning */
|
|
174
174
|
lengths: z.array(z.number()).default([]),
|
|
175
175
|
closed: z.boolean().default(false),
|
|
176
|
-
constantSpeed: z.boolean().default(true)
|
|
176
|
+
constantSpeed: z.boolean().default(true),
|
|
177
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
178
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
177
179
|
});
|
|
178
180
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
179
181
|
var IkConstraint = z.object({
|
|
@@ -242,7 +244,9 @@ var Key = z.object({
|
|
|
242
244
|
t: z.number().nonnegative(),
|
|
243
245
|
v: KeyValue,
|
|
244
246
|
/** easing INTO the next key */
|
|
245
|
-
ease: Ease.default("ease_in_out")
|
|
247
|
+
ease: Ease.default("ease_in_out"),
|
|
248
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
249
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
246
250
|
});
|
|
247
251
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
248
252
|
var Track = z.discriminatedUnion("target", [
|
|
@@ -429,6 +433,10 @@ function validate(doc) {
|
|
|
429
433
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
430
434
|
}
|
|
431
435
|
}
|
|
436
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
437
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
438
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
439
|
+
}
|
|
432
440
|
const vertexCount2 = (a) => {
|
|
433
441
|
if (a.type === "region") return 4;
|
|
434
442
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -512,6 +520,7 @@ function validate(doc) {
|
|
|
512
520
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
513
521
|
let prev = -1;
|
|
514
522
|
for (const k of t.keys) {
|
|
523
|
+
if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
|
|
515
524
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
516
525
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
517
526
|
if (t.target === "control") {
|
|
@@ -559,7 +568,7 @@ function parse(input) {
|
|
|
559
568
|
// src/rig-api.ts
|
|
560
569
|
import { z as z3 } from "zod";
|
|
561
570
|
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
562
|
-
import { dirname as dirname8, join as join9, relative as
|
|
571
|
+
import { dirname as dirname8, join as join9, relative as relative3, isAbsolute as isAbsolute2 } from "node:path";
|
|
563
572
|
|
|
564
573
|
// src/rig-io.ts
|
|
565
574
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -582,8 +591,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
|
|
|
582
591
|
// src/rig-queue.ts
|
|
583
592
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
584
593
|
import { resolve } from "node:path";
|
|
585
|
-
var pending
|
|
586
|
-
|
|
594
|
+
var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
|
|
595
|
+
pending: /* @__PURE__ */ new Map(),
|
|
596
|
+
active: new AsyncLocalStorage()
|
|
597
|
+
};
|
|
587
598
|
async function withRigQueue(path, op) {
|
|
588
599
|
const key = resolve(path);
|
|
589
600
|
const held = active.getStore();
|
|
@@ -739,8 +750,8 @@ function sample(track, t, loop = false) {
|
|
|
739
750
|
}
|
|
740
751
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
741
752
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
742
|
-
const
|
|
743
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
753
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
754
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
744
755
|
}
|
|
745
756
|
return a.v;
|
|
746
757
|
}
|
|
@@ -832,6 +843,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
832
843
|
if (!parent) return local;
|
|
833
844
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
834
845
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
846
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
847
|
+
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);
|
|
848
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
849
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
850
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
851
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
852
|
+
}
|
|
835
853
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
836
854
|
}
|
|
837
855
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -864,11 +882,11 @@ var localOf = (doc, pose, b) => {
|
|
|
864
882
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
865
883
|
};
|
|
866
884
|
};
|
|
867
|
-
function updateSubtree(skel, root2) {
|
|
885
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
868
886
|
const doc = skel.doc;
|
|
869
887
|
const kids = /* @__PURE__ */ new Map();
|
|
870
888
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
871
|
-
const stack = [root2];
|
|
889
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
872
890
|
while (stack.length) {
|
|
873
891
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
874
892
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
@@ -1111,7 +1129,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
1111
1129
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
1112
1130
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
1113
1131
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
1114
|
-
const
|
|
1132
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1133
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
1115
1134
|
for (const id of c.bones) {
|
|
1116
1135
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
1117
1136
|
if (c.local) {
|
|
@@ -1273,6 +1292,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1273
1292
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1274
1293
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1275
1294
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1295
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1276
1296
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1277
1297
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1278
1298
|
let sum = 0;
|
|
@@ -1307,9 +1327,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1307
1327
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1308
1328
|
}
|
|
1309
1329
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1330
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1310
1331
|
for (let i = 0; i < bones.length; i++) {
|
|
1311
1332
|
const { b, st } = bones[i];
|
|
1312
|
-
let w = { ...
|
|
1333
|
+
let w = { ...inputWorlds[i] };
|
|
1313
1334
|
w.tx += (boneX - w.tx) * mixX;
|
|
1314
1335
|
w.ty += (boneY - w.ty) * mixY;
|
|
1315
1336
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1339,7 +1360,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1339
1360
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1340
1361
|
w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
|
|
1341
1362
|
}
|
|
1342
|
-
|
|
1363
|
+
solvedWorlds.set(b.id, w);
|
|
1364
|
+
}
|
|
1365
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1366
|
+
const world = solvedWorlds.get(b.id);
|
|
1367
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1343
1368
|
}
|
|
1344
1369
|
}
|
|
1345
1370
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1368,9 +1393,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1368
1393
|
case "onlyTranslation":
|
|
1369
1394
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1370
1395
|
case "noRotationOrReflection": {
|
|
1371
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1396
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1372
1397
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1373
|
-
|
|
1398
|
+
if (lenX * lenX <= 1e-4) {
|
|
1399
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1400
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1401
|
+
}
|
|
1402
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1403
|
+
return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
|
|
1374
1404
|
}
|
|
1375
1405
|
case "noScale":
|
|
1376
1406
|
case "noScaleOrReflection": {
|
|
@@ -1396,7 +1426,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1396
1426
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1397
1427
|
st.shearX = 0;
|
|
1398
1428
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1399
|
-
|
|
1429
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1430
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1431
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1432
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1433
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1434
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1435
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1436
|
+
}
|
|
1437
|
+
st.world = world;
|
|
1438
|
+
updateSubtree(skel, bone.id, false);
|
|
1400
1439
|
}
|
|
1401
1440
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1402
1441
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1971,8 +2010,9 @@ function createDoc(name, width, height, notes) {
|
|
|
1971
2010
|
function createSymbol(name, asset, margin = 0.15, notes) {
|
|
1972
2011
|
const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
|
|
1973
2012
|
const width = asset.width + 2 * mx, height = asset.height + 2 * my;
|
|
1974
|
-
const
|
|
1975
|
-
const
|
|
2013
|
+
const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
2014
|
+
const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
|
|
2015
|
+
const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
|
|
1976
2016
|
let doc = createDoc(name, width, height, notes);
|
|
1977
2017
|
doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
|
|
1978
2018
|
doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
|
|
@@ -2155,7 +2195,7 @@ function removeSlot(doc, id) {
|
|
|
2155
2195
|
return patchDrawOrder({
|
|
2156
2196
|
...doc,
|
|
2157
2197
|
slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
|
|
2158
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2198
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2159
2199
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2160
2200
|
}, (o) => o.filter((s) => s !== id));
|
|
2161
2201
|
}
|
|
@@ -2551,7 +2591,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2551
2591
|
return {
|
|
2552
2592
|
...doc,
|
|
2553
2593
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2554
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2594
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2555
2595
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2556
2596
|
};
|
|
2557
2597
|
}
|
|
@@ -2728,7 +2768,7 @@ function setGoal(doc, bone, x, y, opts) {
|
|
|
2728
2768
|
|
|
2729
2769
|
// src/rig-import-layers.ts
|
|
2730
2770
|
import { readdir, readFile } from "node:fs/promises";
|
|
2731
|
-
import { join as join2, basename as basename2 } from "node:path";
|
|
2771
|
+
import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
|
|
2732
2772
|
var DEFAULT_PARENT = {
|
|
2733
2773
|
head: "neck",
|
|
2734
2774
|
neck: "torso",
|
|
@@ -2754,6 +2794,10 @@ function pngSize(buf) {
|
|
|
2754
2794
|
if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
|
|
2755
2795
|
return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
|
|
2756
2796
|
}
|
|
2797
|
+
function outsideDir(dir, file) {
|
|
2798
|
+
const r = relative(dir, join2(dir, file));
|
|
2799
|
+
return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
|
|
2800
|
+
}
|
|
2757
2801
|
async function importLayers(doc, dir, relPrefix = "layers") {
|
|
2758
2802
|
let specs;
|
|
2759
2803
|
try {
|
|
@@ -2766,6 +2810,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
|
|
|
2766
2810
|
let next = doc;
|
|
2767
2811
|
const parsed = [];
|
|
2768
2812
|
for (const s of specs) {
|
|
2813
|
+
if (outsideDir(dir, s.file)) throw new Error(`layers.json file "${s.file}" is outside the layers folder (${dir})`);
|
|
2769
2814
|
const buf = await readFile(join2(dir, s.file));
|
|
2770
2815
|
const [w, h] = pngSize(buf);
|
|
2771
2816
|
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,21 +4202,26 @@ 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;
|
|
4163
4211
|
var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
|
|
4164
|
-
function importSpine(
|
|
4212
|
+
function importSpine(json2, atlasText, opts = {}) {
|
|
4165
4213
|
const warnings = [];
|
|
4166
4214
|
const atlas = parseAtlas(atlasText);
|
|
4167
|
-
const version = String(
|
|
4215
|
+
const version = String(json2.skeleton?.spine ?? "4.0");
|
|
4216
|
+
if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4168
4217
|
const major = parseFloat(version);
|
|
4169
|
-
const bounds = { x:
|
|
4218
|
+
const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
|
|
4170
4219
|
const margin = opts.margin ?? 0;
|
|
4171
4220
|
let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
|
|
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}`;
|
|
@@ -4182,7 +4232,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4182
4232
|
}
|
|
4183
4233
|
return id;
|
|
4184
4234
|
};
|
|
4185
|
-
const spineBones =
|
|
4235
|
+
const spineBones = json2.bones ?? [];
|
|
4186
4236
|
const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
|
|
4187
4237
|
const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
|
|
4188
4238
|
for (const b of spineBones) {
|
|
@@ -4195,14 +4245,18 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4195
4245
|
...b.color ? { color: b.color } : {}
|
|
4196
4246
|
});
|
|
4197
4247
|
}
|
|
4198
|
-
const skins = Array.isArray(
|
|
4248
|
+
const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.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,19 +4265,23 @@ 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) =>
|
|
4215
|
-
|
|
4268
|
+
const attachmentRef = (slot, name) => {
|
|
4269
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4270
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4271
|
+
};
|
|
4272
|
+
const spineSlots = json2.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";
|
|
4218
4275
|
const slots = spineSlots.map((s, i) => ({
|
|
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 = (json2.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 = (
|
|
4451
|
+
const transform = (json2.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 = (
|
|
4471
|
+
const path = (json2.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,17 +4503,35 @@ 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
|
};
|
|
4408
4524
|
const animations = [];
|
|
4409
|
-
for (const [animName, a] of Object.entries(
|
|
4525
|
+
for (const [animName, a] of Object.entries(json2.animations ?? {})) {
|
|
4410
4526
|
const tracks = [], events = [];
|
|
4411
4527
|
let duration = 0;
|
|
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));
|
|
@@ -4659,7 +4803,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4659
4803
|
const build = () => ({
|
|
4660
4804
|
format: "energy8.rig",
|
|
4661
4805
|
version: 2,
|
|
4662
|
-
meta: { name: opts.name ??
|
|
4806
|
+
meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
|
|
4663
4807
|
assets: [...assets.values()],
|
|
4664
4808
|
bones,
|
|
4665
4809
|
slots,
|
|
@@ -4712,26 +4856,33 @@ async function firstExisting(candidates, what) {
|
|
|
4712
4856
|
throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
|
|
4713
4857
|
}
|
|
4714
4858
|
async function importSpineProject(opts) {
|
|
4715
|
-
const
|
|
4859
|
+
const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
|
|
4716
4860
|
const stem = opts.json.replace(/\.json$/i, "");
|
|
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(json2, 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 });
|
|
@@ -5419,8 +5571,8 @@ async function load(path) {
|
|
|
5419
5571
|
}
|
|
5420
5572
|
var save = (path, doc) => saveJson(path, doc);
|
|
5421
5573
|
var exists2 = (p) => stat2(p).then(() => true, () => false);
|
|
5422
|
-
var resolveFrom = (rigPath, p) =>
|
|
5423
|
-
var srcFor = (rigPath, file) =>
|
|
5574
|
+
var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
|
|
5575
|
+
var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
|
|
5424
5576
|
var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
|
|
5425
5577
|
async function pngInfo(rigPath, file) {
|
|
5426
5578
|
const abs = resolveFrom(rigPath, file);
|
|
@@ -5532,10 +5684,11 @@ tool(
|
|
|
5532
5684
|
var KeyIn = z3.object({
|
|
5533
5685
|
t: z3.number().describe("seconds"),
|
|
5534
5686
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5687
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5535
5688
|
ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
|
|
5536
5689
|
});
|
|
5537
5690
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5538
|
-
var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5691
|
+
var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5539
5692
|
tool(
|
|
5540
5693
|
"rig_create",
|
|
5541
5694
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5659,19 +5812,20 @@ tool(
|
|
|
5659
5812
|
);
|
|
5660
5813
|
tool(
|
|
5661
5814
|
"rig_import_spine",
|
|
5662
|
-
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text;
|
|
5815
|
+
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
|
|
5663
5816
|
{
|
|
5664
5817
|
path: Path,
|
|
5665
5818
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5666
5819
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5667
5820
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5821
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5668
5822
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5669
5823
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5670
5824
|
notes: z3.string().optional()
|
|
5671
5825
|
},
|
|
5672
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5826
|
+
async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5673
5827
|
try {
|
|
5674
|
-
const r = await importSpineProject({ json: resolveFrom(path,
|
|
5828
|
+
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 });
|
|
5675
5829
|
const res = await create(path, parse(r.doc));
|
|
5676
5830
|
if (r.warnings.length) res.text += `
|
|
5677
5831
|
warnings:
|
|
@@ -6443,40 +6597,41 @@ tool(
|
|
|
6443
6597
|
}
|
|
6444
6598
|
);
|
|
6445
6599
|
|
|
6600
|
+
// src/rig-version.ts
|
|
6601
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
6602
|
+
async function rigVersion(abs) {
|
|
6603
|
+
try {
|
|
6604
|
+
const s = await stat3(abs);
|
|
6605
|
+
return `${s.mtimeMs}:${s.size}`;
|
|
6606
|
+
} catch {
|
|
6607
|
+
return "missing";
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
|
|
6446
6611
|
// src/editor/server.ts
|
|
6447
6612
|
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" };
|
|
6448
|
-
|
|
6613
|
+
var json = (res, status, body) => {
|
|
6614
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
6615
|
+
res.end(JSON.stringify(body));
|
|
6616
|
+
};
|
|
6617
|
+
function createEditorHandler(opts) {
|
|
6449
6618
|
const root2 = resolve2(opts.root);
|
|
6450
6619
|
const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
|
|
6451
6620
|
const inside = (rel) => {
|
|
6452
6621
|
const abs = resolve2(root2, rel);
|
|
6453
|
-
return abs === root2 || abs.startsWith(root2 +
|
|
6454
|
-
};
|
|
6455
|
-
const version = async (abs) => {
|
|
6456
|
-
try {
|
|
6457
|
-
const s = await stat3(abs);
|
|
6458
|
-
return `${s.mtimeMs}:${s.size}`;
|
|
6459
|
-
} catch {
|
|
6460
|
-
return "missing";
|
|
6461
|
-
}
|
|
6622
|
+
return abs === root2 || abs.startsWith(root2 + sep2) ? abs : void 0;
|
|
6462
6623
|
};
|
|
6463
6624
|
const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
|
|
6464
6625
|
const readDocAt = async (abs) => {
|
|
6465
|
-
let v = await
|
|
6626
|
+
let v = await rigVersion(abs);
|
|
6466
6627
|
for (let i = 0; i < 3; i++) {
|
|
6467
6628
|
const doc = await readDoc(abs);
|
|
6468
|
-
const after = await
|
|
6629
|
+
const after = await rigVersion(abs);
|
|
6469
6630
|
if (after === v) return { doc, version: v };
|
|
6470
6631
|
v = after;
|
|
6471
6632
|
}
|
|
6472
6633
|
return { doc: await readDoc(abs), version: v };
|
|
6473
6634
|
};
|
|
6474
|
-
const json = (res, status, body2) => {
|
|
6475
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
6476
|
-
res.end(JSON.stringify(body2));
|
|
6477
|
-
};
|
|
6478
|
-
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6479
|
-
const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
|
|
6480
6635
|
const clients = /* @__PURE__ */ new Map();
|
|
6481
6636
|
const watchers = /* @__PURE__ */ new Map();
|
|
6482
6637
|
function ensureWatch(abs) {
|
|
@@ -6488,7 +6643,7 @@ async function startEditorServer(opts) {
|
|
|
6488
6643
|
const schedule = () => {
|
|
6489
6644
|
clearTimeout(entry.timer);
|
|
6490
6645
|
entry.timer = setTimeout(async () => {
|
|
6491
|
-
const v = await
|
|
6646
|
+
const v = await rigVersion(abs);
|
|
6492
6647
|
if (v === entry.last) return;
|
|
6493
6648
|
entry.last = v;
|
|
6494
6649
|
for (const res of clients.get(abs) ?? []) res.write(`event: changed
|
|
@@ -6497,7 +6652,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6497
6652
|
`);
|
|
6498
6653
|
}, 50);
|
|
6499
6654
|
};
|
|
6500
|
-
|
|
6655
|
+
rigVersion(abs).then((v) => {
|
|
6501
6656
|
entry.last = v;
|
|
6502
6657
|
});
|
|
6503
6658
|
watchers.set(abs, entry);
|
|
@@ -6523,7 +6678,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6523
6678
|
name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
|
|
6524
6679
|
} catch {
|
|
6525
6680
|
}
|
|
6526
|
-
out.push({ rig:
|
|
6681
|
+
out.push({ rig: relative4(root2, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
|
|
6527
6682
|
}
|
|
6528
6683
|
};
|
|
6529
6684
|
await walk(root2);
|
|
@@ -6557,20 +6712,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6557
6712
|
}
|
|
6558
6713
|
return Buffer.concat(chunks);
|
|
6559
6714
|
}
|
|
6560
|
-
|
|
6715
|
+
async function handle(req, res, rawUrl = req.url ?? "/") {
|
|
6561
6716
|
try {
|
|
6562
|
-
|
|
6563
|
-
const url = new URL(req.url ?? "/", "http://x");
|
|
6717
|
+
const url = new URL(rawUrl, "http://x");
|
|
6564
6718
|
if (url.pathname === "/") {
|
|
6565
|
-
res.writeHead(302, { location: "
|
|
6566
|
-
return res.end();
|
|
6719
|
+
res.writeHead(302, { location: "editor.html" + url.search });
|
|
6720
|
+
return void res.end();
|
|
6721
|
+
}
|
|
6722
|
+
if (url.pathname.startsWith("/api/")) {
|
|
6723
|
+
await api(url, req, res);
|
|
6724
|
+
return;
|
|
6567
6725
|
}
|
|
6568
|
-
if (url.pathname.startsWith("/api/")) return await api(url, req, res);
|
|
6569
6726
|
const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
|
|
6570
6727
|
const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
|
|
6571
6728
|
if (!abs) {
|
|
6572
6729
|
res.writeHead(403);
|
|
6573
|
-
return res.end("forbidden");
|
|
6730
|
+
return void res.end("forbidden");
|
|
6574
6731
|
}
|
|
6575
6732
|
try {
|
|
6576
6733
|
const data = await readFile10(abs);
|
|
@@ -6588,7 +6745,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6588
6745
|
}
|
|
6589
6746
|
json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
|
|
6590
6747
|
}
|
|
6591
|
-
}
|
|
6748
|
+
}
|
|
6592
6749
|
async function api(url, req, res) {
|
|
6593
6750
|
const rigOf = (rel) => {
|
|
6594
6751
|
if (!rel) return void 0;
|
|
@@ -6605,7 +6762,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6605
6762
|
res.writeHead(403);
|
|
6606
6763
|
return res.end();
|
|
6607
6764
|
}
|
|
6608
|
-
if (!await
|
|
6765
|
+
if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root2}` });
|
|
6609
6766
|
ensureWatch(abs);
|
|
6610
6767
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
6611
6768
|
res.write(`: connected
|
|
@@ -6627,7 +6784,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6627
6784
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6628
6785
|
ensureWatch(abs);
|
|
6629
6786
|
return await withRigQueue(abs, async () => {
|
|
6630
|
-
if (baseVersion !== void 0 && await
|
|
6787
|
+
if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
|
|
6631
6788
|
const at = await readDocAt(abs);
|
|
6632
6789
|
return json(res, 409, {
|
|
6633
6790
|
isError: true,
|
|
@@ -6636,8 +6793,15 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6636
6793
|
text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
|
|
6637
6794
|
});
|
|
6638
6795
|
}
|
|
6639
|
-
const
|
|
6640
|
-
|
|
6796
|
+
const tool2 = url.pathname.slice(10);
|
|
6797
|
+
let given = { ...args ?? {}, path: abs };
|
|
6798
|
+
try {
|
|
6799
|
+
if (opts.args) given = opts.args(tool2, given);
|
|
6800
|
+
} catch (e) {
|
|
6801
|
+
return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
|
|
6802
|
+
}
|
|
6803
|
+
const r = await callTool(tool2, given);
|
|
6804
|
+
return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
|
|
6641
6805
|
});
|
|
6642
6806
|
}
|
|
6643
6807
|
if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
|
|
@@ -6648,11 +6812,11 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6648
6812
|
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" });
|
|
6649
6813
|
const abs = inside(rig);
|
|
6650
6814
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6651
|
-
if (await
|
|
6815
|
+
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` });
|
|
6652
6816
|
await mkdir7(dirname9(abs), { recursive: true });
|
|
6653
6817
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6654
6818
|
ensureWatch(abs);
|
|
6655
|
-
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await
|
|
6819
|
+
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
|
|
6656
6820
|
}
|
|
6657
6821
|
if (url.pathname === "/api/files" && req.method === "GET") {
|
|
6658
6822
|
const abs = rigOf(url.searchParams.get("rig"));
|
|
@@ -6666,8 +6830,8 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6666
6830
|
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` });
|
|
6667
6831
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
|
|
6668
6832
|
const target = join10(dirname9(abs), name);
|
|
6669
|
-
if (!inside(
|
|
6670
|
-
if (await
|
|
6833
|
+
if (!inside(relative4(root2, target))) return json(res, 403, { isError: true, text: "file outside root" });
|
|
6834
|
+
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` });
|
|
6671
6835
|
const data = await bytes(req, UPLOAD_MAX);
|
|
6672
6836
|
if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
|
|
6673
6837
|
let size;
|
|
@@ -6681,6 +6845,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6681
6845
|
}
|
|
6682
6846
|
json(res, 404, { isError: true, text: "unknown api route" });
|
|
6683
6847
|
}
|
|
6848
|
+
return { handle, close() {
|
|
6849
|
+
for (const w of watchers.values()) {
|
|
6850
|
+
clearTimeout(w.timer);
|
|
6851
|
+
w.w.close();
|
|
6852
|
+
}
|
|
6853
|
+
for (const set of clients.values()) for (const c of set) c.end();
|
|
6854
|
+
} };
|
|
6855
|
+
}
|
|
6856
|
+
async function startEditorServer(opts) {
|
|
6857
|
+
const editor = createEditorHandler(opts);
|
|
6858
|
+
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6859
|
+
const server = createServer((req, res) => {
|
|
6860
|
+
if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
|
|
6861
|
+
return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
|
|
6862
|
+
void editor.handle(req, res);
|
|
6863
|
+
});
|
|
6684
6864
|
await new Promise((ok, fail2) => {
|
|
6685
6865
|
server.once("error", fail2);
|
|
6686
6866
|
server.listen(opts.port ?? 8765, "127.0.0.1", () => {
|
|
@@ -6691,15 +6871,12 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6691
6871
|
const port = server.address().port;
|
|
6692
6872
|
allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
|
|
6693
6873
|
return { port, url: `http://127.0.0.1:${port}`, close: async () => {
|
|
6694
|
-
|
|
6695
|
-
clearTimeout(w.timer);
|
|
6696
|
-
w.w.close();
|
|
6697
|
-
}
|
|
6698
|
-
for (const set of clients.values()) for (const c of set) c.end();
|
|
6874
|
+
editor.close();
|
|
6699
6875
|
await new Promise((r) => server.close(() => r()));
|
|
6700
6876
|
} };
|
|
6701
6877
|
}
|
|
6702
6878
|
export {
|
|
6879
|
+
createEditorHandler,
|
|
6703
6880
|
startEditorServer
|
|
6704
6881
|
};
|
|
6705
6882
|
//# sourceMappingURL=editor-entry.js.map
|