@energy8platform/golem 0.6.0 → 0.8.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 +14 -0
- package/dist/editor.css +3 -4
- package/dist/editor.js +202 -101
- package/dist/lib/cli.js +225 -118
- 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 +6865 -0
- package/dist/lib/e8-agent.js.map +7 -0
- package/dist/lib/e8-client.js +116 -0
- package/dist/lib/e8-host.js +7005 -0
- package/dist/lib/e8-host.js.map +7 -0
- package/dist/lib/e8-runtime.js +2079 -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 +34 -32
- package/dist/lib/editor/timeline.d.ts +15 -0
- package/dist/lib/editor-entry.d.ts +1 -1
- package/dist/lib/editor-entry.js +226 -118
- package/dist/lib/editor-entry.js.map +4 -4
- package/dist/lib/harness.js +54 -1
- package/dist/lib/interactive-editor.js.map +1 -1
- package/dist/lib/rig-check.d.ts +11 -0
- package/dist/lib/rig-control-qa.d.ts +8 -8
- package/dist/lib/rig-format.d.ts +184 -184
- package/dist/lib/rig-import-layers.d.ts +2 -0
- package/dist/lib/rig-symbol.d.ts +5 -2
- package/dist/lib/rig-tools.d.ts +2 -1
- package/dist/lib/rig-version.d.ts +12 -0
- package/dist/lib/runtime.js +60 -3
- package/dist/lib/runtime.js.map +3 -3
- package/dist/lib/tool-schema.d.ts +3 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +179 -58
- 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 +116 -0
- package/skills/golem-symbol-animation/references/packed-delivery.md +88 -0
- package/skills/golem-symbol-animation/references/quantitative-qa.md +109 -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
|
|
@@ -251,7 +251,7 @@ var Key = z.object({
|
|
|
251
251
|
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
252
252
|
componentEase: z.array(Ease).min(1).optional()
|
|
253
253
|
});
|
|
254
|
-
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
254
|
+
var Interpolation = { interpolation: z.enum(["segment", "pchip", "spline"]).optional() };
|
|
255
255
|
var Track = z.discriminatedUnion("target", [
|
|
256
256
|
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
257
257
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -571,7 +571,7 @@ function parse(input) {
|
|
|
571
571
|
// src/rig-api.ts
|
|
572
572
|
import { z as z3 } from "zod";
|
|
573
573
|
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
574
|
-
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";
|
|
575
575
|
|
|
576
576
|
// src/rig-io.ts
|
|
577
577
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -594,8 +594,10 @@ var saveJson = (path, value) => saveBytes(path, JSON.stringify(value, null, 2));
|
|
|
594
594
|
// src/rig-queue.ts
|
|
595
595
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
596
596
|
import { resolve } from "node:path";
|
|
597
|
-
var pending
|
|
598
|
-
|
|
597
|
+
var { pending, active } = globalThis[Symbol.for("golem.rigQueue")] ??= {
|
|
598
|
+
pending: /* @__PURE__ */ new Map(),
|
|
599
|
+
active: new AsyncLocalStorage()
|
|
600
|
+
};
|
|
599
601
|
async function withRigQueue(path, op) {
|
|
600
602
|
const key = resolve(path);
|
|
601
603
|
const held = active.getStore();
|
|
@@ -735,6 +737,57 @@ function smoothValue(track, i, t, component, loop) {
|
|
|
735
737
|
const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
|
|
736
738
|
return (2 * u3 - 3 * u2 + 1) * v(i) + (u3 - 2 * u2 + u) * h(i) * tangent(i) + (-2 * u3 + 3 * u2) * v(i + 1) + (u3 - u2) * h(i) * tangent(i + 1);
|
|
737
739
|
}
|
|
740
|
+
function tridiagonal(a, b, c, r) {
|
|
741
|
+
const n = b.length, cp = [], x = [];
|
|
742
|
+
let beta = b[0];
|
|
743
|
+
x[0] = r[0] / beta;
|
|
744
|
+
for (let i = 1; i < n; i++) {
|
|
745
|
+
cp[i] = c[i - 1] / beta;
|
|
746
|
+
beta = b[i] - a[i] * cp[i];
|
|
747
|
+
x[i] = (r[i] - a[i] * x[i - 1]) / beta;
|
|
748
|
+
}
|
|
749
|
+
for (let i = n - 2; i >= 0; i--) x[i] -= cp[i + 1] * x[i + 1];
|
|
750
|
+
return x;
|
|
751
|
+
}
|
|
752
|
+
function splineMoments(t, v, periodic) {
|
|
753
|
+
const n = t.length, h = (j2) => t[j2 + 1] - t[j2], d = (j2) => (v[j2 + 1] - v[j2]) / h(j2);
|
|
754
|
+
if (!periodic) {
|
|
755
|
+
const a2 = [], b2 = [], c2 = [], r2 = [];
|
|
756
|
+
for (let i = 0; i < n; i++) {
|
|
757
|
+
const hp = i > 0 ? h(i - 1) : 0, hn = i < n - 1 ? h(i) : 0;
|
|
758
|
+
a2.push(hp);
|
|
759
|
+
b2.push(2 * (hp + hn));
|
|
760
|
+
c2.push(hn);
|
|
761
|
+
r2.push(6 * ((i < n - 1 ? d(i) : 0) - (i > 0 ? d(i - 1) : 0)));
|
|
762
|
+
}
|
|
763
|
+
return tridiagonal(a2, b2, c2, r2);
|
|
764
|
+
}
|
|
765
|
+
const m = n - 1, a = [], b = [], c = [], r = [];
|
|
766
|
+
for (let i = 0; i < m; i++) {
|
|
767
|
+
const p = (i + m - 1) % m;
|
|
768
|
+
a.push(h(p));
|
|
769
|
+
b.push(2 * (h(p) + h(i)));
|
|
770
|
+
c.push(h(i));
|
|
771
|
+
r.push(6 * (d(i) - d(p)));
|
|
772
|
+
}
|
|
773
|
+
const top = a[0], bottom = c[m - 1], gamma = -b[0];
|
|
774
|
+
const bb = [...b];
|
|
775
|
+
bb[0] -= gamma;
|
|
776
|
+
bb[m - 1] -= bottom * top / gamma;
|
|
777
|
+
const x = tridiagonal(a, bb, c, r), u = new Array(m).fill(0);
|
|
778
|
+
u[0] = gamma;
|
|
779
|
+
u[m - 1] = bottom;
|
|
780
|
+
const z4 = tridiagonal(a, bb, c, u);
|
|
781
|
+
const f = (x[0] + top * x[m - 1] / gamma) / (1 + z4[0] + top * z4[m - 1] / gamma);
|
|
782
|
+
const M = x.map((xi, i) => xi - f * z4[i]);
|
|
783
|
+
return [...M, M[0]];
|
|
784
|
+
}
|
|
785
|
+
function splineValue(track, i, t, component, loop) {
|
|
786
|
+
const k = track.keys, ts = k.map((x) => x.t), vs = k.map((x) => component === void 0 ? x.v : x.v[component]);
|
|
787
|
+
const M = splineMoments(ts, vs, loop && k.length > 3 && vs[0] === vs[k.length - 1]);
|
|
788
|
+
const H = ts[i + 1] - ts[i], A = (ts[i + 1] - t) / H, B = (t - ts[i]) / H;
|
|
789
|
+
return A * vs[i] + B * vs[i + 1] + ((A ** 3 - A) * M[i] + (B ** 3 - B) * M[i + 1]) * H * H / 6;
|
|
790
|
+
}
|
|
738
791
|
function sample(track, t, loop = false) {
|
|
739
792
|
const keys = track.keys;
|
|
740
793
|
if (t <= keys[0].t) return keys[0].v;
|
|
@@ -746,11 +799,13 @@ function sample(track, t, loop = false) {
|
|
|
746
799
|
if (a.ease === "step") return a.v;
|
|
747
800
|
if (typeof a.v === "number" && typeof b.v === "number") {
|
|
748
801
|
if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
|
|
802
|
+
if (track.interpolation === "spline") return splineValue(track, i, t, void 0, loop);
|
|
749
803
|
const u = ease(a.ease)((t - a.t) / (b.t - a.t));
|
|
750
804
|
return a.v + (b.v - a.v) * u;
|
|
751
805
|
}
|
|
752
806
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
753
807
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
808
|
+
if (track.interpolation === "spline") return a.v.map((_, j2) => splineValue(track, i, t, j2, loop));
|
|
754
809
|
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
755
810
|
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
756
811
|
}
|
|
@@ -2011,8 +2066,9 @@ function createDoc(name, width, height, notes) {
|
|
|
2011
2066
|
function createSymbol(name, asset, margin = 0.15, notes) {
|
|
2012
2067
|
const mx = Math.ceil(asset.width * margin), my = Math.ceil(asset.height * margin);
|
|
2013
2068
|
const width = asset.width + 2 * mx, height = asset.height + 2 * my;
|
|
2014
|
-
const
|
|
2015
|
-
const
|
|
2069
|
+
const stem = asset.src.replace(/^.*\//, "").replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
2070
|
+
const fromFile = stem.replace(/^[^a-z]+/, "") || (stem ? `symbol_${stem}` : "");
|
|
2071
|
+
const id = asset.id ?? (ID_RE.test(fromFile) ? fromFile : ID_RE.test(name) ? name : "symbol");
|
|
2016
2072
|
let doc = createDoc(name, width, height, notes);
|
|
2017
2073
|
doc = { ...doc, bones: [{ ...doc.bones[0], transform: { ...doc.bones[0].transform, x: width / 2, y: height - my } }] };
|
|
2018
2074
|
doc = { ...doc, assets: [RigSchema.shape.assets.element.parse({ id, src: asset.src, width: asset.width, height: asset.height })] };
|
|
@@ -2481,7 +2537,8 @@ function adjustTrack(doc, animId, track, adj) {
|
|
|
2481
2537
|
return { ...k, t: r42(adj.timeScale ? k.t * adj.timeScale : k.t), v: r42(v) };
|
|
2482
2538
|
});
|
|
2483
2539
|
const duration = Math.max(anim.duration, keys[keys.length - 1].t);
|
|
2484
|
-
const
|
|
2540
|
+
const interpolation = track.interpolation ?? tr.interpolation;
|
|
2541
|
+
const next = { ...anim, duration, tracks: anim.tracks.map((t) => same(t) ? { ...t, keys, ...interpolation && { interpolation } } : t), preset: void 0 };
|
|
2485
2542
|
return { ...doc, animations: doc.animations.map((a) => a.id === animId ? next : a) };
|
|
2486
2543
|
}
|
|
2487
2544
|
function removeAnimation(doc, id) {
|
|
@@ -2768,7 +2825,7 @@ function setGoal(doc, bone, x, y, opts) {
|
|
|
2768
2825
|
|
|
2769
2826
|
// src/rig-import-layers.ts
|
|
2770
2827
|
import { readdir, readFile } from "node:fs/promises";
|
|
2771
|
-
import { join as join2, basename as basename2 } from "node:path";
|
|
2828
|
+
import { join as join2, basename as basename2, isAbsolute, relative, sep } from "node:path";
|
|
2772
2829
|
var DEFAULT_PARENT = {
|
|
2773
2830
|
head: "neck",
|
|
2774
2831
|
neck: "torso",
|
|
@@ -2794,6 +2851,10 @@ function pngSize(buf) {
|
|
|
2794
2851
|
if (buf.readUInt32BE(12) !== 1229472850) throw new Error("not a PNG (IHDR missing)");
|
|
2795
2852
|
return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
|
|
2796
2853
|
}
|
|
2854
|
+
function outsideDir(dir, file) {
|
|
2855
|
+
const r = relative(dir, join2(dir, file));
|
|
2856
|
+
return isAbsolute(file) || r === ".." || r.startsWith(".." + sep);
|
|
2857
|
+
}
|
|
2797
2858
|
async function importLayers(doc, dir, relPrefix = "layers") {
|
|
2798
2859
|
let specs;
|
|
2799
2860
|
try {
|
|
@@ -2806,6 +2867,7 @@ async function importLayers(doc, dir, relPrefix = "layers") {
|
|
|
2806
2867
|
let next = doc;
|
|
2807
2868
|
const parsed = [];
|
|
2808
2869
|
for (const s2 of specs) {
|
|
2870
|
+
if (outsideDir(dir, s2.file)) throw new Error(`layers.json file "${s2.file}" is outside the layers folder (${dir})`);
|
|
2809
2871
|
const buf = await readFile(join2(dir, s2.file));
|
|
2810
2872
|
const [w, h] = pngSize(buf);
|
|
2811
2873
|
const m = basename2(s2.file, ".png").match(/^(?:(\d+)_)?(.+)$/);
|
|
@@ -2893,19 +2955,20 @@ var requireSlot = (doc, id) => {
|
|
|
2893
2955
|
if (!s2) throw new Error(`unknown slot "${id}"`);
|
|
2894
2956
|
return s2;
|
|
2895
2957
|
};
|
|
2896
|
-
function sampled(duration, n, f
|
|
2958
|
+
function sampled(duration, n, f) {
|
|
2897
2959
|
const keys = [];
|
|
2898
2960
|
for (let i = 0; i <= n; i++) {
|
|
2899
2961
|
const t = +(duration * i / n).toFixed(4);
|
|
2900
|
-
keys.push({ t, v: +f(t).toFixed(4) + 0, ease:
|
|
2962
|
+
keys.push({ t, v: +f(t).toFixed(4) + 0, ease: "linear" });
|
|
2901
2963
|
}
|
|
2902
2964
|
return keys;
|
|
2903
2965
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2966
|
+
var smooth = "spline";
|
|
2967
|
+
function boneTrack(bone, prop, keys, interpolation) {
|
|
2968
|
+
return { target: "bone", id: bone.id, prop, keys, ...interpolation && { interpolation } };
|
|
2906
2969
|
}
|
|
2907
|
-
function deformTrack(slot, prop, keys) {
|
|
2908
|
-
return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys } : void 0;
|
|
2970
|
+
function deformTrack(slot, prop, keys, interpolation) {
|
|
2971
|
+
return slot.attachment ? { target: "deform", id: slot.attachment, prop, keys, ...interpolation && { interpolation } } : void 0;
|
|
2909
2972
|
}
|
|
2910
2973
|
var pushTrack = (tracks, t) => {
|
|
2911
2974
|
if (t) tracks.push(t);
|
|
@@ -2929,18 +2992,18 @@ var idleBreathing = {
|
|
|
2929
2992
|
const armsR = byRole(doc, "arm_r", "arm_upper_r");
|
|
2930
2993
|
const chest = slotOfBone(doc, torso);
|
|
2931
2994
|
if (chest?.attachment) {
|
|
2932
|
-
pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t),
|
|
2995
|
+
pushTrack(tracks, deformTrack(chest, "bulge", sampled(T2, 8, (t) => p.amplitude * Math.sin(w * t)), smooth));
|
|
2933
2996
|
} else {
|
|
2934
2997
|
const r = torso.transform;
|
|
2935
|
-
tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t),
|
|
2936
|
-
tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t),
|
|
2998
|
+
tracks.push(boneTrack(torso, "scaleY", sampled(T2, 8, (t) => r.scaleY + p.amplitude * Math.sin(w * t)), smooth));
|
|
2999
|
+
tracks.push(boneTrack(torso, "scaleX", sampled(T2, 8, (t) => r.scaleX - p.amplitude * 0.4 * Math.sin(w * t)), smooth));
|
|
2937
3000
|
}
|
|
2938
3001
|
for (const h of heads)
|
|
2939
|
-
tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4),
|
|
3002
|
+
tracks.push(boneTrack(h, "y", sampled(T2, 8, (t) => h.transform.y - p.headBob * Math.sin(w * t + 0.4)), smooth));
|
|
2940
3003
|
for (const a of armsL)
|
|
2941
|
-
tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8),
|
|
3004
|
+
tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation + p.armSway * Math.sin(w * t + 0.8)), smooth));
|
|
2942
3005
|
for (const a of armsR)
|
|
2943
|
-
tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8),
|
|
3006
|
+
tracks.push(boneTrack(a, "rotation", sampled(T2, 8, (t) => a.transform.rotation - p.armSway * Math.sin(w * t + 0.8)), smooth));
|
|
2944
3007
|
return finish(this.name, p.id, T2, true, tracks, p);
|
|
2945
3008
|
}
|
|
2946
3009
|
};
|
|
@@ -2949,22 +3012,20 @@ var winBounce = {
|
|
|
2949
3012
|
defaults: { id: "win", duration: 1.2, height: 40, bounces: 2, squash: 0.15 },
|
|
2950
3013
|
build(doc, p) {
|
|
2951
3014
|
const b = root(doc), r = b.transform, tracks = [];
|
|
2952
|
-
const yK = [[0, r.y,
|
|
2953
|
-
const sxK = [[0, r.scaleX, "ease_out"]];
|
|
2954
|
-
const syK = [[0, r.scaleY, "ease_out"]];
|
|
3015
|
+
const yK = [[0, r.y]], sxK = [[0, r.scaleX]], syK = [[0, r.scaleY]];
|
|
2955
3016
|
const per = p.duration / p.bounces;
|
|
2956
3017
|
for (let i = 0; i < p.bounces; i++) {
|
|
2957
3018
|
const t0 = i * per, h = p.height * 0.55 ** i, s2 = p.squash * 0.7 ** i;
|
|
2958
|
-
sxK.push([t0 + per * 0.08, r.scaleX * (1 + s2)
|
|
2959
|
-
syK.push([t0 + per * 0.08, r.scaleY * (1 - s2)
|
|
2960
|
-
yK.push([t0 + per * 0.08, r.y
|
|
3019
|
+
sxK.push([t0 + per * 0.08, r.scaleX * (1 + s2)], [t0 + per * 0.5, r.scaleX * (1 - s2 * 0.6)], [t0 + per * 0.95, r.scaleX * (1 + s2)]);
|
|
3020
|
+
syK.push([t0 + per * 0.08, r.scaleY * (1 - s2)], [t0 + per * 0.5, r.scaleY * (1 + s2 * 0.6)], [t0 + per * 0.95, r.scaleY * (1 - s2)]);
|
|
3021
|
+
yK.push([t0 + per * 0.08, r.y], [t0 + per * 0.5, r.y - h], [t0 + per * 0.95, r.y]);
|
|
2961
3022
|
}
|
|
2962
3023
|
yK.push([p.duration, r.y]);
|
|
2963
3024
|
sxK.push([p.duration, r.scaleX]);
|
|
2964
3025
|
syK.push([p.duration, r.scaleY]);
|
|
2965
|
-
tracks.push(boneTrack(b, "y", K(...yK)), boneTrack(b, "scaleX", K(...sxK)), boneTrack(b, "scaleY", K(...syK)));
|
|
3026
|
+
tracks.push(boneTrack(b, "y", K(...yK), "pchip"), boneTrack(b, "scaleX", K(...sxK), smooth), boneTrack(b, "scaleY", K(...syK), smooth));
|
|
2966
3027
|
for (const h of byRole(doc, "head"))
|
|
2967
|
-
tracks.push(boneTrack(h, "rotation", K([0, h.transform.rotation], [p.duration * 0.25, h.transform.rotation - 6
|
|
3028
|
+
tracks.push(boneTrack(h, "rotation", K([0, h.transform.rotation], [p.duration * 0.25, h.transform.rotation - 6], [p.duration * 0.6, h.transform.rotation + 4], [p.duration, h.transform.rotation]), smooth));
|
|
2968
3029
|
const anim = finish(this.name, p.id, p.duration, false, tracks, p);
|
|
2969
3030
|
anim.events.push({ t: 0, name: "win_start" }, { t: per * 0.95, name: "land" });
|
|
2970
3031
|
return anim;
|
|
@@ -2977,8 +3038,8 @@ var anticipationShake = {
|
|
|
2977
3038
|
const b = root(doc), r = b.transform, tracks = [];
|
|
2978
3039
|
const n = Math.ceil(p.duration * p.frequency * 4);
|
|
2979
3040
|
const env = (t) => p.grow ? t / p.duration : 1;
|
|
2980
|
-
tracks.push(boneTrack(b, "x", sampled(p.duration, n, (t) => r.x + p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t),
|
|
2981
|
-
tracks.push(boneTrack(b, "rotation", sampled(p.duration, n, (t) => r.rotation + 0.6 * p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t + 1.2),
|
|
3041
|
+
tracks.push(boneTrack(b, "x", sampled(p.duration, n, (t) => r.x + p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t)), smooth));
|
|
3042
|
+
tracks.push(boneTrack(b, "rotation", sampled(p.duration, n, (t) => r.rotation + 0.6 * p.amplitude * env(t) * Math.sin(2 * Math.PI * p.frequency * t + 1.2)), smooth));
|
|
2982
3043
|
tracks.push(boneTrack(b, "scaleX", K([0, r.scaleX, "ease_in"], [p.duration, r.scaleX * p.scaleUp])));
|
|
2983
3044
|
tracks.push(boneTrack(b, "scaleY", K([0, r.scaleY, "ease_in"], [p.duration, r.scaleY * p.scaleUp])));
|
|
2984
3045
|
return finish(this.name, p.id, p.duration, false, tracks, p);
|
|
@@ -3003,6 +3064,7 @@ var mouthTalk = {
|
|
|
3003
3064
|
if (!p.frames.length) throw new Error("mouth_talk: frames must not be empty");
|
|
3004
3065
|
const n = Math.max(1, Math.round(p.duration * p.rate));
|
|
3005
3066
|
const keys = S(...Array.from({ length: n }, (_, i) => [+(i / p.rate).toFixed(4), p.frames[i % p.frames.length]]));
|
|
3067
|
+
if (keys.at(-1).t < p.duration && keys.at(-1).v !== keys[0].v) keys.push(...S([p.duration, keys[0].v]));
|
|
3006
3068
|
return finish(this.name, p.id, p.duration, true, [slotTrack(slot, "attachment", keys)], p);
|
|
3007
3069
|
}
|
|
3008
3070
|
};
|
|
@@ -3020,10 +3082,10 @@ var symbolIdle = {
|
|
|
3020
3082
|
build(doc, p) {
|
|
3021
3083
|
const b = root(doc), r = b.transform, T2 = p.period, w = 2 * Math.PI / T2, tracks = [];
|
|
3022
3084
|
const rise = (t) => 0.5 - 0.5 * Math.cos(w * t);
|
|
3023
|
-
tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t),
|
|
3024
|
-
if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t),
|
|
3085
|
+
tracks.push(boneTrack(b, "y", sampled(T2, 8, (t) => r.y - p.float * rise(t)), smooth));
|
|
3086
|
+
if (p.tilt) tracks.push(boneTrack(b, "rotation", sampled(T2, 8, (t) => r.rotation + p.tilt * Math.sin(w * t)), smooth));
|
|
3025
3087
|
if (p.pulse) for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment))
|
|
3026
|
-
pushTrack(tracks, deformTrack(s2, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t),
|
|
3088
|
+
pushTrack(tracks, deformTrack(s2, "bulge", sampled(T2, 8, (t) => p.pulse * rise(t)), smooth));
|
|
3027
3089
|
return finish(this.name, p.id, T2, true, tracks, p);
|
|
3028
3090
|
}
|
|
3029
3091
|
};
|
|
@@ -3033,12 +3095,12 @@ var symbolWin = {
|
|
|
3033
3095
|
build(doc, p) {
|
|
3034
3096
|
const b = root(doc), r = b.transform, d = p.duration, tracks = [];
|
|
3035
3097
|
for (const [prop, rest] of [["scaleX", r.scaleX], ["scaleY", r.scaleY]])
|
|
3036
|
-
tracks.push(boneTrack(b, prop, K([0, rest
|
|
3098
|
+
tracks.push(boneTrack(b, prop, K([0, rest], [d * 0.2, rest * (1 + p.pop)], [d * 0.5, rest * (1 - p.pop * 0.3)], [d, rest]), smooth));
|
|
3037
3099
|
if (p.wobble) {
|
|
3038
3100
|
const n = Math.max(8, p.wobbles * 4);
|
|
3039
|
-
const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d)
|
|
3101
|
+
const keys = sampled(d, n, (t) => p.wobble * Math.exp(-3 * t / d) * Math.sin(2 * Math.PI * p.wobbles * t / d));
|
|
3040
3102
|
keys[keys.length - 1].v = 0;
|
|
3041
|
-
for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment)) pushTrack(tracks, deformTrack(s2, "sway", keys));
|
|
3103
|
+
for (const s2 of doc.slots.filter((s3) => s3.bone === b.id && s3.attachment)) pushTrack(tracks, deformTrack(s2, "sway", keys, smooth));
|
|
3042
3104
|
}
|
|
3043
3105
|
const anim = finish(this.name, p.id, d, false, tracks, p);
|
|
3044
3106
|
anim.events.push({ t: 0, name: "win_start" });
|
|
@@ -3052,11 +3114,11 @@ var weightShift = {
|
|
|
3052
3114
|
const T2 = p.period, w = 2 * Math.PI / T2, N = 16, tracks = [];
|
|
3053
3115
|
const hips = byRole(doc, "hips")[0] ?? root(doc), hr = hips.transform;
|
|
3054
3116
|
const s2 = (t) => Math.sin(w * t);
|
|
3055
|
-
tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s2(t))));
|
|
3056
|
-
if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s2(t) ** 2)));
|
|
3117
|
+
tracks.push(boneTrack(hips, "x", sampled(T2, N, (t) => hr.x + p.shift * s2(t)), smooth));
|
|
3118
|
+
if (p.dip) tracks.push(boneTrack(hips, "y", sampled(T2, N, (t) => hr.y + p.dip * s2(t) ** 2), smooth));
|
|
3057
3119
|
const torso = byRole(doc, "torso", "body")[0];
|
|
3058
|
-
if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s2(t))));
|
|
3059
|
-
for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s2(t))));
|
|
3120
|
+
if (torso && torso !== hips) tracks.push(boneTrack(torso, "rotation", sampled(T2, N, (t) => torso.transform.rotation - p.lean * s2(t)), smooth));
|
|
3121
|
+
for (const h of byRole(doc, "head")) tracks.push(boneTrack(h, "rotation", sampled(T2, N, (t) => h.transform.rotation + p.lean / 2 * s2(t)), smooth));
|
|
3060
3122
|
for (const leg of byRole(doc, "leg_l", "leg_r")) {
|
|
3061
3123
|
const slot = slotOfBone(doc, leg), att = slot && doc.attachments.find((a) => a.id === slot.attachment && a.type === "region");
|
|
3062
3124
|
const asset = att && att.type === "region" ? doc.assets.find((a) => a.id === att.asset) : void 0;
|
|
@@ -3066,8 +3128,8 @@ var weightShift = {
|
|
|
3066
3128
|
length = Math.max(...[[0, -h / 2], [w2 / 2, 0], [0, h / 2], [-w2 / 2, 0]].map(([x, y]) => Math.hypot(...apply(m, x, y))));
|
|
3067
3129
|
}
|
|
3068
3130
|
if (!(length > p.shift)) continue;
|
|
3069
|
-
tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s2(t) / length) * 180 / Math.PI)));
|
|
3070
|
-
if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s2(t) ** 2)));
|
|
3131
|
+
tracks.push(boneTrack(leg, "rotation", sampled(T2, N, (t) => leg.transform.rotation + Math.asin(p.shift * s2(t) / length) * 180 / Math.PI), smooth));
|
|
3132
|
+
if (p.dip) tracks.push(boneTrack(leg, "y", sampled(T2, N, (t) => leg.transform.y - p.dip * s2(t) ** 2), smooth));
|
|
3071
3133
|
}
|
|
3072
3134
|
return finish(this.name, p.id, T2, true, tracks, p);
|
|
3073
3135
|
}
|
|
@@ -3310,13 +3372,13 @@ function motionQuality(doc, anim, opts = {}) {
|
|
|
3310
3372
|
const before = delta(values(k.v), values(p.v)), after = delta(values(n.v), values(k.v));
|
|
3311
3373
|
const pass = before.some((v, j2) => v * after[j2] > 1e-6);
|
|
3312
3374
|
const speed = Math.max(magnitude(before) / (k.t - p.t), magnitude(after) / (n.t - k.t));
|
|
3313
|
-
if (pass && Math.max(magnitude(left), magnitude(right)) < speed * 0.02) report("pass_key_stop", id, `motion stops at a pass-through key; use track interpolation pchip or matched tangents`, k.t);
|
|
3375
|
+
if (pass && Math.max(magnitude(left), magnitude(right)) < speed * 0.02) report("pass_key_stop", id, `motion stops at a pass-through key; use track interpolation pchip or spline, or matched tangents`, k.t);
|
|
3314
3376
|
const jump = magnitude(delta(left, right));
|
|
3315
3377
|
if (jump > Math.max(0.1, speed * 0.1)) report("velocity_jump", id, `velocity jumps ${jump.toFixed(3)} units/s across this key`, k.t);
|
|
3316
3378
|
const acceleration = (a, b, c) => values(at(b)).map((v, j2) => (values(at(c))[j2] - 2 * v + values(at(a))[j2]) / (h * h));
|
|
3317
3379
|
const al = acceleration(k.t - 2 * h, k.t - h, k.t), ar = acceleration(k.t, k.t + h, k.t + 2 * h);
|
|
3318
3380
|
const aj = magnitude(delta(al, ar));
|
|
3319
|
-
if (aj > Math.max(1, speed / Math.min(k.t - p.t, n.t - k.t) * 2)) report("acceleration_jump", id, `acceleration changes by ${aj.toFixed(2)} units/s\xB2 at this key; PCHIP guarantees velocity, not acceleration continuity`, k.t, "info");
|
|
3381
|
+
if (aj > Math.max(1, speed / Math.min(k.t - p.t, n.t - k.t) * 2)) report("acceleration_jump", id, `acceleration changes by ${aj.toFixed(2)} units/s\xB2 at this key; PCHIP guarantees velocity, not acceleration continuity \u2014 spline does both`, k.t, "info");
|
|
3320
3382
|
}
|
|
3321
3383
|
}
|
|
3322
3384
|
const steps = Math.max(2, Math.ceil(D * 60));
|
|
@@ -3798,6 +3860,17 @@ function checkRig(doc, opts = {}) {
|
|
|
3798
3860
|
out.push({ level: "warn", code: "never_visible", subject: s2.id, message: `slot "${s2.id}" has no attachment and no animation ever shows one` });
|
|
3799
3861
|
return out;
|
|
3800
3862
|
}
|
|
3863
|
+
function summarizeIgnoring(findings, rules, max) {
|
|
3864
|
+
const hits = (r, f) => r.code === f.code && (r.subject ?? f.subject) === f.subject && (r.animation ?? f.animation) === f.animation;
|
|
3865
|
+
const kept = findings.filter((f) => !rules.some((r) => hits(r, f)));
|
|
3866
|
+
const idle = rules.filter((r) => !findings.some((f) => hits(r, f)));
|
|
3867
|
+
const notes = [
|
|
3868
|
+
...kept.length < findings.length ? [` (${findings.length - kept.length} ignored)`] : [],
|
|
3869
|
+
...idle.map((r) => ` ignore matched nothing: ${JSON.stringify(r)}`)
|
|
3870
|
+
];
|
|
3871
|
+
return [summarize(kept, max), ...notes].join("\n");
|
|
3872
|
+
}
|
|
3873
|
+
var where = (f) => `${f.subject}${f.animation ? ` in ${f.animation}` : ""}${f.time !== void 0 ? ` @${+f.time.toFixed(4)}s` : ""}`;
|
|
3801
3874
|
function summarize(findings, max = 12) {
|
|
3802
3875
|
if (!findings.length) return "check: OK";
|
|
3803
3876
|
const rank = { error: 0, warn: 1, info: 2 };
|
|
@@ -3812,7 +3885,7 @@ function summarize(findings, max = 12) {
|
|
|
3812
3885
|
const lines = [];
|
|
3813
3886
|
for (const f of sorted) {
|
|
3814
3887
|
const shown = shownPerCode.get(f.code) ?? 0;
|
|
3815
|
-
if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code}: ${f.message}`);
|
|
3888
|
+
if (shown < PER_CODE) lines.push(` [${f.level}] ${f.code} ${where(f)}: ${f.message}`);
|
|
3816
3889
|
else if (shown === PER_CODE) lines.push(` \u2026 ${codeTotal.get(f.code) - PER_CODE} more ${f.code}`);
|
|
3817
3890
|
shownPerCode.set(f.code, shown + 1);
|
|
3818
3891
|
}
|
|
@@ -4003,7 +4076,7 @@ function packRects(rects, opts = {}) {
|
|
|
4003
4076
|
shelf = Math.max(shelf, r.height);
|
|
4004
4077
|
}
|
|
4005
4078
|
const height = y + shelf + pad;
|
|
4006
|
-
if (height <= max) return { width, height, frames };
|
|
4079
|
+
if (height <= max) return { width: Math.max(pad, ...[...frames.values()].map(([fx, , fw]) => fx + fw)) + pad, height, frames };
|
|
4007
4080
|
if (width >= max) throw new Error(`atlas does not fit in ${max}\xD7${max} (needs ${width}\xD7${height})`);
|
|
4008
4081
|
width *= 2;
|
|
4009
4082
|
}
|
|
@@ -4062,7 +4135,7 @@ async function buildAtlas(doc, assetsDir, outDir, opts = {}) {
|
|
|
4062
4135
|
|
|
4063
4136
|
// src/spine-import.ts
|
|
4064
4137
|
import { readFile as readFile7, stat, writeFile as writeFile5 } from "node:fs/promises";
|
|
4065
|
-
import { basename as basename4, dirname as dirname6, extname, join as join8, relative } from "node:path";
|
|
4138
|
+
import { basename as basename4, dirname as dirname6, extname, join as join8, relative as relative2 } from "node:path";
|
|
4066
4139
|
|
|
4067
4140
|
// src/ktx2.ts
|
|
4068
4141
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
@@ -4211,13 +4284,13 @@ var Ids = class {
|
|
|
4211
4284
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4212
4285
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
4213
4286
|
var hex8 = (c) => `#${(c ?? "ffffffff").toLowerCase().padEnd(8, "f").slice(0, 8)}`;
|
|
4214
|
-
function importSpine(
|
|
4287
|
+
function importSpine(json2, atlasText, opts = {}) {
|
|
4215
4288
|
const warnings = [];
|
|
4216
4289
|
const atlas = parseAtlas(atlasText);
|
|
4217
|
-
const version = String(
|
|
4218
|
-
if (
|
|
4290
|
+
const version = String(json2.skeleton?.spine ?? "4.0");
|
|
4291
|
+
if (json2.physics?.length) warnings.push(`${json2.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4219
4292
|
const major = parseFloat(version);
|
|
4220
|
-
const bounds = { x:
|
|
4293
|
+
const bounds = { x: json2.skeleton?.x ?? 0, y: json2.skeleton?.y ?? 0, w: json2.skeleton?.width ?? 0, h: json2.skeleton?.height ?? 0 };
|
|
4221
4294
|
const margin = opts.margin ?? 0;
|
|
4222
4295
|
let originX = -bounds.x + margin, originY = bounds.y + bounds.h + margin;
|
|
4223
4296
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
@@ -4234,7 +4307,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4234
4307
|
}
|
|
4235
4308
|
return id;
|
|
4236
4309
|
};
|
|
4237
|
-
const spineBones =
|
|
4310
|
+
const spineBones = json2.bones ?? [];
|
|
4238
4311
|
const boneIndexByName = new Map(spineBones.map((b, i) => [b.name, i]));
|
|
4239
4312
|
const bones = [{ id: ROOT, transform: { x: r4(originX), y: r4(originY) }, role: "root" }];
|
|
4240
4313
|
for (const b of spineBones) {
|
|
@@ -4247,7 +4320,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4247
4320
|
...b.color ? { color: b.color } : {}
|
|
4248
4321
|
});
|
|
4249
4322
|
}
|
|
4250
|
-
const skins = Array.isArray(
|
|
4323
|
+
const skins = Array.isArray(json2.skins) ? json2.skins : Object.entries(json2.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4251
4324
|
const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
|
|
4252
4325
|
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
|
|
4253
4326
|
const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
|
|
@@ -4271,7 +4344,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4271
4344
|
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4272
4345
|
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4273
4346
|
};
|
|
4274
|
-
const spineSlots =
|
|
4347
|
+
const spineSlots = json2.slots ?? [];
|
|
4275
4348
|
const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
|
|
4276
4349
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
4277
4350
|
const slots = spineSlots.map((s2, i) => ({
|
|
@@ -4437,7 +4510,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4437
4510
|
}
|
|
4438
4511
|
}
|
|
4439
4512
|
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
|
|
4440
|
-
const ik = (
|
|
4513
|
+
const ik = (json2.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4441
4514
|
id: ikIds.get(c.name),
|
|
4442
4515
|
order: c.order ?? 0,
|
|
4443
4516
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4450,7 +4523,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4450
4523
|
stretch: !!c.stretch,
|
|
4451
4524
|
uniform: !!c.uniform
|
|
4452
4525
|
}));
|
|
4453
|
-
const transform = (
|
|
4526
|
+
const transform = (json2.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4454
4527
|
id: tcIds.get(c.name),
|
|
4455
4528
|
order: c.order ?? 0,
|
|
4456
4529
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4470,7 +4543,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4470
4543
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4471
4544
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4472
4545
|
}));
|
|
4473
|
-
const path = (
|
|
4546
|
+
const path = (json2.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4474
4547
|
id: pcIds.get(c.name),
|
|
4475
4548
|
order: c.order ?? 0,
|
|
4476
4549
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4524,7 +4597,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4524
4597
|
return keys;
|
|
4525
4598
|
};
|
|
4526
4599
|
const animations = [];
|
|
4527
|
-
for (const [animName, a] of Object.entries(
|
|
4600
|
+
for (const [animName, a] of Object.entries(json2.animations ?? {})) {
|
|
4528
4601
|
const tracks = [], events = [];
|
|
4529
4602
|
let duration = 0;
|
|
4530
4603
|
const seen = (frames) => {
|
|
@@ -4805,7 +4878,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4805
4878
|
const build = () => ({
|
|
4806
4879
|
format: "energy8.rig",
|
|
4807
4880
|
version: 2,
|
|
4808
|
-
meta: { name: opts.name ??
|
|
4881
|
+
meta: { name: opts.name ?? json2.skeleton?.hash ?? "spine", width, height, notes: opts.notes ?? `imported from Spine ${version}` },
|
|
4809
4882
|
assets: [...assets.values()],
|
|
4810
4883
|
bones,
|
|
4811
4884
|
slots,
|
|
@@ -4858,7 +4931,7 @@ async function firstExisting(candidates, what) {
|
|
|
4858
4931
|
throw new Error(`${what} not found, tried: ${candidates.join(", ")}`);
|
|
4859
4932
|
}
|
|
4860
4933
|
async function importSpineProject(opts) {
|
|
4861
|
-
const
|
|
4934
|
+
const json2 = JSON.parse(await readFile7(opts.json, "utf8"));
|
|
4862
4935
|
const stem = opts.json.replace(/\.json$/i, "");
|
|
4863
4936
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4864
4937
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
@@ -4868,6 +4941,7 @@ async function importSpineProject(opts) {
|
|
|
4868
4941
|
const rigDir = dirname6(opts.rigPath);
|
|
4869
4942
|
const pagePaths = /* @__PURE__ */ new Map();
|
|
4870
4943
|
for (const page2 of pages) {
|
|
4944
|
+
if (!opts.image && outsideDir(dirname6(atlasPath), page2.name)) throw new Error(`atlas page "${page2.name}" is outside the atlas folder (${dirname6(atlasPath)})`);
|
|
4871
4945
|
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4872
4946
|
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), page2.name), pageStem + ".png", pageStem + ".ktx2", ...pages.length === 1 ? [stem + ".png", stem + ".ktx2"] : []], `atlas page image ${page2.name}`);
|
|
4873
4947
|
let pngPath = imagePath;
|
|
@@ -4880,10 +4954,10 @@ async function importSpineProject(opts) {
|
|
|
4880
4954
|
}
|
|
4881
4955
|
pagePaths.set(page2.name, pngPath);
|
|
4882
4956
|
}
|
|
4883
|
-
const pageImage = (page2) =>
|
|
4957
|
+
const pageImage = (page2) => relative2(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4884
4958
|
const name = opts.name ?? basename4(stem);
|
|
4885
4959
|
const imagePaths = [...pagePaths.values()];
|
|
4886
|
-
return { ...importSpine(
|
|
4960
|
+
return { ...importSpine(json2, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4887
4961
|
}
|
|
4888
4962
|
|
|
4889
4963
|
// src/rig-templates.ts
|
|
@@ -5573,8 +5647,8 @@ async function load(path) {
|
|
|
5573
5647
|
}
|
|
5574
5648
|
var save = (path, doc) => saveJson(path, doc);
|
|
5575
5649
|
var exists2 = (p) => stat2(p).then(() => true, () => false);
|
|
5576
|
-
var resolveFrom = (rigPath, p) =>
|
|
5577
|
-
var srcFor = (rigPath, file) =>
|
|
5650
|
+
var resolveFrom = (rigPath, p) => isAbsolute2(p) ? p : join9(dirname8(rigPath), p);
|
|
5651
|
+
var srcFor = (rigPath, file) => relative3(dirname8(rigPath), file).split("\\").join("/");
|
|
5578
5652
|
var libraryFor = (rigPath, lib) => lib ? resolveFrom(rigPath, lib) : join9(dirname8(rigPath), "presets.json");
|
|
5579
5653
|
async function pngInfo(rigPath, file) {
|
|
5580
5654
|
const abs = resolveFrom(rigPath, file);
|
|
@@ -5609,7 +5683,8 @@ function previewCheck(doc, animation) {
|
|
|
5609
5683
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5610
5684
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5611
5685
|
}
|
|
5612
|
-
var
|
|
5686
|
+
var TrackId = { target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
|
|
5687
|
+
var TrackRef = { interpolation: z3.enum(["segment", "pchip", "spline"]).optional().describe("segment: per-key easing; pchip: smooth through numeric keys, never past them (continuous velocity); spline: C2 cubic through them, continuous acceleration too, may overshoot between keys \u2014 for sampled curves"), ...TrackId };
|
|
5613
5688
|
tool(
|
|
5614
5689
|
"rig_set_control",
|
|
5615
5690
|
"Create or replace a semantic control with a finite min/max/default range. Animate target=control, prop=value. Changing a range requires matching binding grids.",
|
|
@@ -5802,11 +5877,21 @@ tool(
|
|
|
5802
5877
|
);
|
|
5803
5878
|
tool(
|
|
5804
5879
|
"rig_check",
|
|
5805
|
-
"Cheap self-critique without rendering: out_of_canvas (slot leaves the canvas, worst time), static_track, loop_seam, deform_flip (lattice folds over itself \u2014 an error), mesh_fold (a skinned/deformed mesh folds over itself: error when \u2265 5 % of the mesh folds, warning below \u2014 a dense mesh bent hard at a joint pinches the odd hairline triangle), weights_sum (skinned mesh vertex weights not summing to 1), patch_offset (a slot sits away from its asset's source bbox), rest_overlap, unused_asset, never_visible, and for constraints: constraint_dead (every mix is 0 at rest and no track lifts one \u2014 the solver skips it), constraint_cycle (the target is a bone the constraint drives, or hangs off one: it reads what it writes \u2014 an error), ik_unreachable (the target sits beyond the chain's reach for over 20 % of an animation, so the limb hangs straight), path_missing (a path constraint whose target slot never shows a path attachment \u2014 an error). Motion diagnostics also cover pass_key_stop, velocity_jump, acceleration_jump, loop_velocity and constraint_order; provide secondaryBones, frontOf slot pairs and named attachment UV landmarks for brief-specific frozen-motion, ordering and swap-registration checks. Findings are sorted by severity. Run it before spending vision tokens on a preview.",
|
|
5806
|
-
{
|
|
5807
|
-
|
|
5880
|
+
"Cheap self-critique without rendering: out_of_canvas (slot leaves the canvas, worst time), static_track, loop_seam, deform_flip (lattice folds over itself \u2014 an error), mesh_fold (a skinned/deformed mesh folds over itself: error when \u2265 5 % of the mesh folds, warning below \u2014 a dense mesh bent hard at a joint pinches the odd hairline triangle), weights_sum (skinned mesh vertex weights not summing to 1), patch_offset (a slot sits away from its asset's source bbox), rest_overlap, unused_asset, never_visible, and for constraints: constraint_dead (every mix is 0 at rest and no track lifts one \u2014 the solver skips it), constraint_cycle (the target is a bone the constraint drives, or hangs off one: it reads what it writes \u2014 an error), ik_unreachable (the target sits beyond the chain's reach for over 20 % of an animation, so the limb hangs straight), path_missing (a path constraint whose target slot never shows a path attachment \u2014 an error). Motion diagnostics also cover pass_key_stop, velocity_jump, acceleration_jump, loop_velocity and constraint_order; provide secondaryBones, frontOf slot pairs and named attachment UV landmarks for brief-specific frozen-motion, ordering and swap-registration checks. Findings are sorted by severity; each line names its subject (a track as target.id.prop), animation and time. `ignore` drops findings you have read and accept \u2014 by code, narrowed by subject and animation; the reply counts them and names a rule that matched nothing. Run it before spending vision tokens on a preview.",
|
|
5881
|
+
{
|
|
5882
|
+
path: Path,
|
|
5883
|
+
animation: z3.string().optional().describe("limit to one animation (rest pose is always checked)"),
|
|
5884
|
+
fps: z3.number().positive().optional(),
|
|
5885
|
+
deformFps: z3.number().positive().optional(),
|
|
5886
|
+
secondaryBones: z3.array(z3.string()).optional(),
|
|
5887
|
+
frontOf: z3.array(z3.tuple([z3.string(), z3.string()])).optional(),
|
|
5888
|
+
landmarks: z3.record(z3.record(z3.tuple([z3.number(), z3.number()]))).optional(),
|
|
5889
|
+
landmarkTolerance: z3.number().nonnegative().optional(),
|
|
5890
|
+
ignore: z3.array(z3.object({ code: z3.string(), subject: z3.string().optional(), animation: z3.string().optional() }).strict()).optional()
|
|
5891
|
+
},
|
|
5892
|
+
async ({ path, ignore = [], ...opts }) => {
|
|
5808
5893
|
try {
|
|
5809
|
-
return text(
|
|
5894
|
+
return text(summarizeIgnoring(checkRig(await load(path), opts), ignore, 100));
|
|
5810
5895
|
} catch (e) {
|
|
5811
5896
|
return fail(e);
|
|
5812
5897
|
}
|
|
@@ -5825,9 +5910,9 @@ tool(
|
|
|
5825
5910
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5826
5911
|
notes: z3.string().optional()
|
|
5827
5912
|
},
|
|
5828
|
-
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5913
|
+
async ({ path, json: json2, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5829
5914
|
try {
|
|
5830
|
-
const r = await importSpineProject({ json: resolveFrom(path,
|
|
5915
|
+
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 });
|
|
5831
5916
|
const res = await create(path, parse(r.doc));
|
|
5832
5917
|
if (r.warnings.length) res.text += `
|
|
5833
5918
|
warnings:
|
|
@@ -6423,9 +6508,9 @@ tool(
|
|
|
6423
6508
|
);
|
|
6424
6509
|
tool(
|
|
6425
6510
|
"rig_adjust_track",
|
|
6426
|
-
"Re-tune an existing track without retyping keys: amplitude scales its deviation from the rest value (0.5 = half as strong, 2 = twice; scale props as ratios), offset adds a constant, timeScale stretches its key times (duration grows to fit). Drops the preset link.",
|
|
6511
|
+
"Re-tune an existing track without retyping keys: amplitude scales its deviation from the rest value (0.5 = half as strong, 2 = twice; scale props as ratios), offset adds a constant, timeScale stretches its key times (duration grows to fit), interpolation switches segment / pchip / spline. Drops the preset link.",
|
|
6427
6512
|
{ path: Path, animation: z3.string(), ...TrackRef, amplitude: z3.number().optional(), offset: z3.number().optional(), timeScale: z3.number().positive().optional() },
|
|
6428
|
-
async ({ path, animation, target, id, prop, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop }, adj))
|
|
6513
|
+
async ({ path, animation, target, id, prop, interpolation, ...adj }) => mutate(path, (d) => adjustTrack(d, animation, { target, id, prop, interpolation }, adj))
|
|
6429
6514
|
);
|
|
6430
6515
|
tool(
|
|
6431
6516
|
"rig_set_animation",
|
|
@@ -6443,7 +6528,7 @@ tool(
|
|
|
6443
6528
|
tool(
|
|
6444
6529
|
"rig_remove_keys",
|
|
6445
6530
|
"Remove the keys of one track at the given times; an emptied track is dropped. Drops the preset link.",
|
|
6446
|
-
{ path: Path, animation: z3.string(), ...
|
|
6531
|
+
{ path: Path, animation: z3.string(), ...TrackId, times: z3.array(z3.number()).min(1) },
|
|
6447
6532
|
async ({ path, animation, target, id, prop, times: times2 }) => mutate(path, (d) => removeKeys(d, animation, { target, id, prop }, times2))
|
|
6448
6533
|
);
|
|
6449
6534
|
tool(
|
|
@@ -6599,40 +6684,41 @@ tool(
|
|
|
6599
6684
|
}
|
|
6600
6685
|
);
|
|
6601
6686
|
|
|
6687
|
+
// src/rig-version.ts
|
|
6688
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
6689
|
+
async function rigVersion(abs) {
|
|
6690
|
+
try {
|
|
6691
|
+
const s2 = await stat3(abs);
|
|
6692
|
+
return `${s2.mtimeMs}:${s2.size}`;
|
|
6693
|
+
} catch {
|
|
6694
|
+
return "missing";
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
|
|
6602
6698
|
// src/editor/server.ts
|
|
6603
6699
|
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" };
|
|
6604
|
-
|
|
6700
|
+
var json = (res, status, body) => {
|
|
6701
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
6702
|
+
res.end(JSON.stringify(body));
|
|
6703
|
+
};
|
|
6704
|
+
function createEditorHandler(opts) {
|
|
6605
6705
|
const root3 = resolve2(opts.root);
|
|
6606
6706
|
const assets = resolve2(opts.assets ?? fileURLToPath3(new URL("../..", import.meta.url)));
|
|
6607
6707
|
const inside = (rel) => {
|
|
6608
6708
|
const abs = resolve2(root3, rel);
|
|
6609
|
-
return abs === root3 || abs.startsWith(root3 +
|
|
6610
|
-
};
|
|
6611
|
-
const version = async (abs) => {
|
|
6612
|
-
try {
|
|
6613
|
-
const s2 = await stat3(abs);
|
|
6614
|
-
return `${s2.mtimeMs}:${s2.size}`;
|
|
6615
|
-
} catch {
|
|
6616
|
-
return "missing";
|
|
6617
|
-
}
|
|
6709
|
+
return abs === root3 || abs.startsWith(root3 + sep2) ? abs : void 0;
|
|
6618
6710
|
};
|
|
6619
6711
|
const readDoc = async (abs) => RigDocument.parse(JSON.parse(await readFile10(abs, "utf8")));
|
|
6620
6712
|
const readDocAt = async (abs) => {
|
|
6621
|
-
let v = await
|
|
6713
|
+
let v = await rigVersion(abs);
|
|
6622
6714
|
for (let i = 0; i < 3; i++) {
|
|
6623
6715
|
const doc = await readDoc(abs);
|
|
6624
|
-
const after = await
|
|
6716
|
+
const after = await rigVersion(abs);
|
|
6625
6717
|
if (after === v) return { doc, version: v };
|
|
6626
6718
|
v = after;
|
|
6627
6719
|
}
|
|
6628
6720
|
return { doc: await readDoc(abs), version: v };
|
|
6629
6721
|
};
|
|
6630
|
-
const json = (res, status, body2) => {
|
|
6631
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
6632
|
-
res.end(JSON.stringify(body2));
|
|
6633
|
-
};
|
|
6634
|
-
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6635
|
-
const hostOk = (req) => allowedHosts.has(String(req.headers.host ?? "").toLowerCase());
|
|
6636
6722
|
const clients = /* @__PURE__ */ new Map();
|
|
6637
6723
|
const watchers = /* @__PURE__ */ new Map();
|
|
6638
6724
|
function ensureWatch(abs) {
|
|
@@ -6644,7 +6730,7 @@ async function startEditorServer(opts) {
|
|
|
6644
6730
|
const schedule = () => {
|
|
6645
6731
|
clearTimeout(entry.timer);
|
|
6646
6732
|
entry.timer = setTimeout(async () => {
|
|
6647
|
-
const v = await
|
|
6733
|
+
const v = await rigVersion(abs);
|
|
6648
6734
|
if (v === entry.last) return;
|
|
6649
6735
|
entry.last = v;
|
|
6650
6736
|
for (const res of clients.get(abs) ?? []) res.write(`event: changed
|
|
@@ -6653,7 +6739,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6653
6739
|
`);
|
|
6654
6740
|
}, 50);
|
|
6655
6741
|
};
|
|
6656
|
-
|
|
6742
|
+
rigVersion(abs).then((v) => {
|
|
6657
6743
|
entry.last = v;
|
|
6658
6744
|
});
|
|
6659
6745
|
watchers.set(abs, entry);
|
|
@@ -6679,7 +6765,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6679
6765
|
name = String(JSON.parse(await readFile10(abs, "utf8")).meta?.name ?? "?");
|
|
6680
6766
|
} catch {
|
|
6681
6767
|
}
|
|
6682
|
-
out.push({ rig:
|
|
6768
|
+
out.push({ rig: relative4(root3, abs).split(sep2).join("/"), name, mtimeMs: (await stat4(abs)).mtimeMs });
|
|
6683
6769
|
}
|
|
6684
6770
|
};
|
|
6685
6771
|
await walk(root3);
|
|
@@ -6713,20 +6799,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6713
6799
|
}
|
|
6714
6800
|
return Buffer.concat(chunks);
|
|
6715
6801
|
}
|
|
6716
|
-
|
|
6802
|
+
async function handle(req, res, rawUrl = req.url ?? "/") {
|
|
6717
6803
|
try {
|
|
6718
|
-
|
|
6719
|
-
const url = new URL(req.url ?? "/", "http://x");
|
|
6804
|
+
const url = new URL(rawUrl, "http://x");
|
|
6720
6805
|
if (url.pathname === "/") {
|
|
6721
|
-
res.writeHead(302, { location: "
|
|
6722
|
-
return res.end();
|
|
6806
|
+
res.writeHead(302, { location: "editor.html" + url.search });
|
|
6807
|
+
return void res.end();
|
|
6808
|
+
}
|
|
6809
|
+
if (url.pathname.startsWith("/api/")) {
|
|
6810
|
+
await api(url, req, res);
|
|
6811
|
+
return;
|
|
6723
6812
|
}
|
|
6724
|
-
if (url.pathname.startsWith("/api/")) return await api(url, req, res);
|
|
6725
6813
|
const STATIC = /* @__PURE__ */ new Set(["/editor.html", "/dist/editor.js", "/dist/editor.js.map", "/dist/editor.css", "/dist/editor.css.map"]);
|
|
6726
6814
|
const abs = url.pathname.startsWith("/rig/") ? inside(decodeURIComponent(url.pathname.slice(5))) : STATIC.has(url.pathname) ? join10(assets, url.pathname) : void 0;
|
|
6727
6815
|
if (!abs) {
|
|
6728
6816
|
res.writeHead(403);
|
|
6729
|
-
return res.end("forbidden");
|
|
6817
|
+
return void res.end("forbidden");
|
|
6730
6818
|
}
|
|
6731
6819
|
try {
|
|
6732
6820
|
const data = await readFile10(abs);
|
|
@@ -6744,7 +6832,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6744
6832
|
}
|
|
6745
6833
|
json(res, 500, { isError: true, text: `ERROR: ${e.message}` });
|
|
6746
6834
|
}
|
|
6747
|
-
}
|
|
6835
|
+
}
|
|
6748
6836
|
async function api(url, req, res) {
|
|
6749
6837
|
const rigOf = (rel) => {
|
|
6750
6838
|
if (!rel) return void 0;
|
|
@@ -6761,7 +6849,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6761
6849
|
res.writeHead(403);
|
|
6762
6850
|
return res.end();
|
|
6763
6851
|
}
|
|
6764
|
-
if (!await
|
|
6852
|
+
if (!await stat4(abs).then(() => true, () => false)) return json(res, 404, { isError: true, text: `ERROR: ${url.searchParams.get("rig")} does not exist under ${root3}` });
|
|
6765
6853
|
ensureWatch(abs);
|
|
6766
6854
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
6767
6855
|
res.write(`: connected
|
|
@@ -6783,7 +6871,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6783
6871
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6784
6872
|
ensureWatch(abs);
|
|
6785
6873
|
return await withRigQueue(abs, async () => {
|
|
6786
|
-
if (baseVersion !== void 0 && await
|
|
6874
|
+
if (baseVersion !== void 0 && await rigVersion(abs) !== baseVersion) {
|
|
6787
6875
|
const at = await readDocAt(abs);
|
|
6788
6876
|
return json(res, 409, {
|
|
6789
6877
|
isError: true,
|
|
@@ -6792,8 +6880,15 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6792
6880
|
text: `ERROR: the rig changed on disk (expected ${baseVersion}, found ${at.version}); nothing was written`
|
|
6793
6881
|
});
|
|
6794
6882
|
}
|
|
6795
|
-
const
|
|
6796
|
-
|
|
6883
|
+
const tool2 = url.pathname.slice(10);
|
|
6884
|
+
let given = { ...args2 ?? {}, path: abs };
|
|
6885
|
+
try {
|
|
6886
|
+
if (opts.args) given = opts.args(tool2, given);
|
|
6887
|
+
} catch (e) {
|
|
6888
|
+
return json(res, 400, { isError: true, text: `ERROR: ${e instanceof Error ? e.message : String(e)}` });
|
|
6889
|
+
}
|
|
6890
|
+
const r = await callTool(tool2, given);
|
|
6891
|
+
return json(res, 200, { text: r.text, doc: r.doc, image: r.image?.toString("base64"), isError: r.isError, version: await rigVersion(abs) });
|
|
6797
6892
|
});
|
|
6798
6893
|
}
|
|
6799
6894
|
if (url.pathname === "/api/rigs" && req.method === "GET") return json(res, 200, await listRigs());
|
|
@@ -6804,11 +6899,11 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6804
6899
|
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" });
|
|
6805
6900
|
const abs = inside(rig);
|
|
6806
6901
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6807
|
-
if (await
|
|
6902
|
+
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` });
|
|
6808
6903
|
await mkdir7(dirname9(abs), { recursive: true });
|
|
6809
6904
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6810
6905
|
ensureWatch(abs);
|
|
6811
|
-
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await
|
|
6906
|
+
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await rigVersion(abs) });
|
|
6812
6907
|
}
|
|
6813
6908
|
if (url.pathname === "/api/files" && req.method === "GET") {
|
|
6814
6909
|
const abs = rigOf(url.searchParams.get("rig"));
|
|
@@ -6822,8 +6917,8 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6822
6917
|
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` });
|
|
6823
6918
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("image/png")) return json(res, 415, { isError: true, text: "ERROR: upload needs content-type: image/png" });
|
|
6824
6919
|
const target = join10(dirname9(abs), name);
|
|
6825
|
-
if (!inside(
|
|
6826
|
-
if (await
|
|
6920
|
+
if (!inside(relative4(root3, target))) return json(res, 403, { isError: true, text: "file outside root" });
|
|
6921
|
+
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` });
|
|
6827
6922
|
const data = await bytes(req, UPLOAD_MAX);
|
|
6828
6923
|
if (!data) return json(res, 413, { isError: true, text: `ERROR: ${name} is larger than ${UPLOAD_MAX / 1048576} MB` });
|
|
6829
6924
|
let size;
|
|
@@ -6837,6 +6932,22 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6837
6932
|
}
|
|
6838
6933
|
json(res, 404, { isError: true, text: "unknown api route" });
|
|
6839
6934
|
}
|
|
6935
|
+
return { handle, close() {
|
|
6936
|
+
for (const w of watchers.values()) {
|
|
6937
|
+
clearTimeout(w.timer);
|
|
6938
|
+
w.w.close();
|
|
6939
|
+
}
|
|
6940
|
+
for (const set of clients.values()) for (const c of set) c.end();
|
|
6941
|
+
} };
|
|
6942
|
+
}
|
|
6943
|
+
async function startEditorServer(opts) {
|
|
6944
|
+
const editor = createEditorHandler(opts);
|
|
6945
|
+
let allowedHosts = /* @__PURE__ */ new Set();
|
|
6946
|
+
const server = createServer((req, res) => {
|
|
6947
|
+
if (!allowedHosts.has(String(req.headers.host ?? "").toLowerCase()))
|
|
6948
|
+
return json(res, 403, { isError: true, text: `ERROR: forbidden Host "${req.headers.host ?? ""}" \u2014 this server answers only on 127.0.0.1 / localhost` });
|
|
6949
|
+
void editor.handle(req, res);
|
|
6950
|
+
});
|
|
6840
6951
|
await new Promise((ok, fail2) => {
|
|
6841
6952
|
server.once("error", fail2);
|
|
6842
6953
|
server.listen(opts.port ?? 8765, "127.0.0.1", () => {
|
|
@@ -6847,11 +6958,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6847
6958
|
const port2 = server.address().port;
|
|
6848
6959
|
allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${port2}`, `localhost:${port2}`, `[::1]:${port2}`]);
|
|
6849
6960
|
return { port: port2, url: `http://127.0.0.1:${port2}`, close: async () => {
|
|
6850
|
-
|
|
6851
|
-
clearTimeout(w.timer);
|
|
6852
|
-
w.w.close();
|
|
6853
|
-
}
|
|
6854
|
-
for (const set of clients.values()) for (const c of set) c.end();
|
|
6961
|
+
editor.close();
|
|
6855
6962
|
await new Promise((r) => server.close(() => r()));
|
|
6856
6963
|
} };
|
|
6857
6964
|
}
|