@energy8platform/golem 0.3.0 → 0.5.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 +17 -0
- package/dist/editor.css +50 -0
- package/dist/editor.js +1315 -969
- package/dist/lib/cli.js +278 -19
- package/dist/lib/cli.js.map +4 -4
- package/dist/lib/editor/controls-panel.d.ts +2 -0
- package/dist/lib/editor/store.d.ts +140 -1
- package/dist/lib/editor-entry.js +278 -19
- package/dist/lib/editor-entry.js.map +4 -4
- package/dist/lib/harness.js +167 -5
- package/dist/lib/interactive-bones.d.ts +37 -0
- package/dist/lib/interactive-editor.d.ts +80 -0
- package/dist/lib/interactive-editor.js +744 -0
- package/dist/lib/interactive-editor.js.map +7 -0
- package/dist/lib/rig-anim.d.ts +5 -2
- package/dist/lib/rig-control-qa.d.ts +319 -0
- package/dist/lib/rig-controls.d.ts +13 -0
- package/dist/lib/rig-format.d.ts +912 -0
- package/dist/lib/rig-gates.d.ts +6 -6
- package/dist/lib/rig-runtime.d.ts +39 -2
- package/dist/lib/runtime.d.ts +3 -1
- package/dist/lib/runtime.js +170 -5
- package/dist/lib/runtime.js.map +4 -4
- package/dist/lib/tools.js +274 -15
- package/dist/lib/tools.js.map +4 -4
- package/package.json +7 -1
package/dist/lib/editor-entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/editor/server.ts
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { readFile as readFile10, stat as stat3, readdir as
|
|
3
|
+
import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
4
4
|
import { watch as fsWatch } from "node:fs";
|
|
5
5
|
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -246,6 +246,7 @@ var Key = z.object({
|
|
|
246
246
|
});
|
|
247
247
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
248
248
|
var Track = z.discriminatedUnion("target", [
|
|
249
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
249
250
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
250
251
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
251
252
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -268,6 +269,25 @@ var Animation = z.object({
|
|
|
268
269
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
269
270
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
270
271
|
});
|
|
272
|
+
var RigControl = z.object({
|
|
273
|
+
id: Id,
|
|
274
|
+
label: z.string().optional(),
|
|
275
|
+
min: z.number().finite(),
|
|
276
|
+
max: z.number().finite(),
|
|
277
|
+
default: z.number().finite()
|
|
278
|
+
}).strict();
|
|
279
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
280
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
281
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
282
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
283
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
284
|
+
]);
|
|
285
|
+
var ControlBinding = z.object({
|
|
286
|
+
id: Id,
|
|
287
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
288
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
289
|
+
channels: z.array(ControlChannel).min(1)
|
|
290
|
+
}).strict();
|
|
271
291
|
var Reference = z.object({
|
|
272
292
|
src: z.string(),
|
|
273
293
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -293,7 +313,9 @@ var RigSchema = z.object({
|
|
|
293
313
|
ik: z.array(IkConstraint).default([]),
|
|
294
314
|
transform: z.array(TransformConstraint).default([]),
|
|
295
315
|
path: z.array(PathConstraint).default([]),
|
|
296
|
-
animations: z.array(Animation)
|
|
316
|
+
animations: z.array(Animation),
|
|
317
|
+
controls: z.array(RigControl).optional(),
|
|
318
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
297
319
|
});
|
|
298
320
|
var RigDocument = z.preprocess((input) => {
|
|
299
321
|
const v = input?.version;
|
|
@@ -368,6 +390,8 @@ function validate(doc) {
|
|
|
368
390
|
dup(doc.ik, "ik");
|
|
369
391
|
dup(doc.transform, "transform");
|
|
370
392
|
dup(doc.path, "path");
|
|
393
|
+
dup(doc.controls ?? [], "control");
|
|
394
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
371
395
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
372
396
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
373
397
|
const slots = new Set(doc.slots.map((s) => s.id));
|
|
@@ -442,13 +466,47 @@ function validate(doc) {
|
|
|
442
466
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
443
467
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
444
468
|
}
|
|
469
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
470
|
+
const driven = /* @__PURE__ */ new Set();
|
|
471
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
472
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
473
|
+
for (const b of doc.controlBindings ?? []) {
|
|
474
|
+
const prefix = `control binding "${b.id}"`;
|
|
475
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
476
|
+
for (const axis of b.axes) {
|
|
477
|
+
const c = controls.get(axis.control);
|
|
478
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
479
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
480
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
481
|
+
}
|
|
482
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
483
|
+
for (const ch of b.channels) {
|
|
484
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
485
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
486
|
+
driven.add(key);
|
|
487
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
488
|
+
if (ch.target === "deform") {
|
|
489
|
+
const att = attachments.get(ch.id);
|
|
490
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
491
|
+
else {
|
|
492
|
+
const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
|
|
493
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
497
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
498
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
445
502
|
for (const a of doc.animations) {
|
|
446
503
|
const tracks = /* @__PURE__ */ new Set();
|
|
447
504
|
for (const t of a.tracks) {
|
|
448
505
|
const tid = trackId(t);
|
|
449
506
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
450
507
|
tracks.add(tid);
|
|
451
|
-
|
|
508
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
509
|
+
const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : t.target === "control" ? controls.has(t.id) : true;
|
|
452
510
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
453
511
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
454
512
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
@@ -456,6 +514,10 @@ function validate(doc) {
|
|
|
456
514
|
for (const k of t.keys) {
|
|
457
515
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
458
516
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
517
|
+
if (t.target === "control") {
|
|
518
|
+
const c = controls.get(t.id);
|
|
519
|
+
if (typeof k.v !== "number" || !Number.isFinite(k.v) || c && (k.v < c.min || k.v > c.max)) errs.push(`animation "${a.id}" ${tid}: control key outside its finite range`);
|
|
520
|
+
}
|
|
459
521
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
460
522
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
461
523
|
else if (typeof k.v === "string") {
|
|
@@ -496,7 +558,7 @@ function parse(input) {
|
|
|
496
558
|
|
|
497
559
|
// src/rig-api.ts
|
|
498
560
|
import { z as z3 } from "zod";
|
|
499
|
-
import { readFile as readFile9, writeFile as writeFile7, stat as stat2 } from "node:fs/promises";
|
|
561
|
+
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
500
562
|
import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
|
|
501
563
|
|
|
502
564
|
// src/rig-io.ts
|
|
@@ -544,6 +606,65 @@ async function withRigQueue(path, op) {
|
|
|
544
606
|
}
|
|
545
607
|
}
|
|
546
608
|
|
|
609
|
+
// src/rig-controls.ts
|
|
610
|
+
function controlPose(doc, values2) {
|
|
611
|
+
const out = /* @__PURE__ */ new Map();
|
|
612
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
613
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
614
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
615
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
616
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
function bracket(keys, value) {
|
|
621
|
+
let i = 0;
|
|
622
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
623
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
624
|
+
}
|
|
625
|
+
function sampleBinding(binding, values2) {
|
|
626
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
627
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
628
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
629
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
630
|
+
return binding.channels.map((ch) => {
|
|
631
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
632
|
+
const at = (j2) => {
|
|
633
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
634
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
635
|
+
};
|
|
636
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function resolveControls(doc, input) {
|
|
640
|
+
if (!doc.controlBindings?.length) return input;
|
|
641
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
642
|
+
for (const b of doc.controlBindings) {
|
|
643
|
+
const values2 = b.axes.map((axis) => {
|
|
644
|
+
const c = controls.get(axis.control);
|
|
645
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
646
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
647
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
648
|
+
});
|
|
649
|
+
const sampled2 = sampleBinding(b, values2);
|
|
650
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
651
|
+
}
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
function controlCases(doc, probes) {
|
|
655
|
+
let cases = [{ label: "default", values: {} }];
|
|
656
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
657
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
658
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
659
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
660
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
661
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
662
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
663
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
664
|
+
}
|
|
665
|
+
return cases;
|
|
666
|
+
}
|
|
667
|
+
|
|
547
668
|
// src/rig-anim.ts
|
|
548
669
|
var DEG2 = Math.PI / 180;
|
|
549
670
|
var named = {
|
|
@@ -647,6 +768,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
647
768
|
}
|
|
648
769
|
function restValue(doc, key) {
|
|
649
770
|
const [target, id, prop] = key.split(":");
|
|
771
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
650
772
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
651
773
|
if (target === "slot") {
|
|
652
774
|
const s = doc.slots.find((s2) => s2.id === id);
|
|
@@ -753,7 +875,9 @@ function updateSubtree(skel, root2) {
|
|
|
753
875
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
754
876
|
}
|
|
755
877
|
}
|
|
756
|
-
function evaluate(doc, pose) {
|
|
878
|
+
function evaluate(doc, pose, modifiers) {
|
|
879
|
+
pose = resolveControls(doc, pose);
|
|
880
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
757
881
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
758
882
|
for (const b of boneOrder(doc)) {
|
|
759
883
|
const s = localOf(doc, pose, b);
|
|
@@ -1979,6 +2103,9 @@ function updateBone(doc, id, patch) {
|
|
|
1979
2103
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1980
2104
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1981
2105
|
const cur = doc.bones[i];
|
|
2106
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2107
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2108
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1982
2109
|
const { parent, ...rest } = patch;
|
|
1983
2110
|
const next = RigSchema.shape.bones.element.parse({
|
|
1984
2111
|
...cur,
|
|
@@ -2011,6 +2138,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2011
2138
|
return { ...doc, animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.map((t) => t.target !== "drawOrder" ? t : { ...t, keys: t.keys.map((k) => ({ ...k, v: fn(k.v) })) }) })) };
|
|
2012
2139
|
}
|
|
2013
2140
|
function setSlot(doc, slot) {
|
|
2141
|
+
if (slot.alpha !== void 0) {
|
|
2142
|
+
for (const b of doc.controlBindings ?? [])
|
|
2143
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2144
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2145
|
+
}
|
|
2014
2146
|
const cur = doc.slots.find((s) => s.id === slot.id);
|
|
2015
2147
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2016
2148
|
const base = doc.slots.find((s) => s.id === slot.id);
|
|
@@ -2385,6 +2517,7 @@ function trackSummary(t) {
|
|
|
2385
2517
|
function rename2(doc, kind, id, to) {
|
|
2386
2518
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2387
2519
|
if (to === id) return doc;
|
|
2520
|
+
if (doc.controlBindings) doc = { ...doc, controlBindings: doc.controlBindings.map((b) => ({ ...b, channels: b.channels.map((ch) => ch.id === id && (kind === ch.target || kind === "attachment" && ch.target === "deform") ? { ...ch, id: to } : ch) })) };
|
|
2388
2521
|
const sub = (v) => v === id ? to : v;
|
|
2389
2522
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2390
2523
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2466,6 +2599,8 @@ function describeAttachment(doc, att) {
|
|
|
2466
2599
|
}
|
|
2467
2600
|
function describe(doc, opts = {}) {
|
|
2468
2601
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2602
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2603
|
+
for (const b of doc.controlBindings ?? []) lines.push(`keyforms ${b.id}: ${b.axes.map((a) => `${a.control}[${a.keys.join(",")}]`).join(" \xD7 ")} \u2192 ${b.channels.map((c) => `${c.target}.${c.id}.${c.prop}`).join(", ")}`);
|
|
2469
2604
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2470
2605
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2471
2606
|
const place = (s) => {
|
|
@@ -3158,8 +3293,8 @@ function motionQuality(doc, anim, opts = {}) {
|
|
|
3158
3293
|
if (!skel.attachment.get(front) || !skel.attachment.get(back)) continue;
|
|
3159
3294
|
const visible = (id) => {
|
|
3160
3295
|
const slot = doc.slots.find((s) => s.id === id);
|
|
3161
|
-
const color = pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3162
|
-
return Number(pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3296
|
+
const color = skel.pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3297
|
+
return Number(skel.pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3163
3298
|
};
|
|
3164
3299
|
if (!visible(front) || !visible(back)) continue;
|
|
3165
3300
|
const order = pose.get(pk("drawOrder", "order", "order"));
|
|
@@ -3234,6 +3369,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3234
3369
|
});
|
|
3235
3370
|
const W = doc.meta.width, H = doc.meta.height;
|
|
3236
3371
|
const overflowOf = (pose, slotId, skel) => {
|
|
3372
|
+
pose = skel.pose;
|
|
3237
3373
|
const pts = slotOutline(doc, pose, slotId, 4, skel);
|
|
3238
3374
|
if (!pts) return void 0;
|
|
3239
3375
|
const slot = doc.slots.find((s) => s.id === slotId), alphaV = pose.get(`slot:${slotId}:alpha`);
|
|
@@ -3432,6 +3568,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3432
3568
|
}
|
|
3433
3569
|
}
|
|
3434
3570
|
const vertexKeyed = new Set(a.tracks.filter((t) => t.target === "deform" && t.prop === "vertices").map((t) => t.id));
|
|
3571
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels) if (ch.target === "deform") vertexKeyed.add(ch.id);
|
|
3435
3572
|
const deformMeshes = meshes.filter((att) => vertexKeyed.has(att.id));
|
|
3436
3573
|
const bonesMeshes = meshes.filter((att) => !vertexKeyed.has(att.id));
|
|
3437
3574
|
const meshFolds = /* @__PURE__ */ new Map();
|
|
@@ -3768,11 +3905,13 @@ function diffDocs(a, b) {
|
|
|
3768
3905
|
["attachments", "attachment"],
|
|
3769
3906
|
["ik", "ik"],
|
|
3770
3907
|
["transform", "transform"],
|
|
3771
|
-
["path", "path"]
|
|
3908
|
+
["path", "path"],
|
|
3909
|
+
["controls", "control"],
|
|
3910
|
+
["controlBindings", "control binding"]
|
|
3772
3911
|
];
|
|
3773
3912
|
for (const [kind, name] of kinds) {
|
|
3774
|
-
const ma = new Map(a[kind].map((x) => [x.id, x]));
|
|
3775
|
-
const mb = new Map(b[kind].map((x) => [x.id, x]));
|
|
3913
|
+
const ma = new Map((a[kind] ?? []).map((x) => [x.id, x]));
|
|
3914
|
+
const mb = new Map((b[kind] ?? []).map((x) => [x.id, x]));
|
|
3776
3915
|
for (const [id, x] of mb) if (!ma.has(id)) out.push(`+ ${name} ${id} ${j(x)}`);
|
|
3777
3916
|
for (const [id, x] of ma) {
|
|
3778
3917
|
const y = mb.get(id);
|
|
@@ -4609,7 +4748,7 @@ function extractTemplate(doc, animId, name, groups = {}) {
|
|
|
4609
4748
|
const tracks = [];
|
|
4610
4749
|
const skipped = [];
|
|
4611
4750
|
for (const t of anim.tracks) {
|
|
4612
|
-
if (t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4751
|
+
if (t.target === "control" || t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4613
4752
|
skipped.push(trackId5(t));
|
|
4614
4753
|
continue;
|
|
4615
4754
|
}
|
|
@@ -4984,8 +5123,8 @@ function runGates(doc, config) {
|
|
|
4984
5123
|
let active2 = 0, bad = 0;
|
|
4985
5124
|
for (const t of ts) {
|
|
4986
5125
|
const pose = poseOf(animation, t), s = evaluate(doc, pose);
|
|
4987
|
-
const color = pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
4988
|
-
const alpha = Number(pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
5126
|
+
const color = s.pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
5127
|
+
const alpha = Number(s.pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
4989
5128
|
if (!s.attachment.get(slot.id) || alpha <= 0.01) continue;
|
|
4990
5129
|
active2++;
|
|
4991
5130
|
const v = bone(s, rule.bone, rule.prop === "rotation" ? "rotation" : "position", rule.space)[rule.prop === "y" ? 1 : 0];
|
|
@@ -5207,6 +5346,53 @@ function bakeSecondary(doc, animation, opts) {
|
|
|
5207
5346
|
return next;
|
|
5208
5347
|
}
|
|
5209
5348
|
|
|
5349
|
+
// src/rig-control-qa.ts
|
|
5350
|
+
function snapshot(doc, values2) {
|
|
5351
|
+
const skel = evaluate(doc, controlPose(doc, values2));
|
|
5352
|
+
return doc.slots.flatMap((slot) => {
|
|
5353
|
+
const alpha = Number(skel.pose.get(`slot:${slot.id}:alpha`) ?? slot.alpha);
|
|
5354
|
+
const points = slotOutline(doc, skel.pose, slot.id, 1, skel);
|
|
5355
|
+
return points ? [{ id: slot.id, alpha, points }] : [];
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
function checkControlCases(doc, cases) {
|
|
5359
|
+
const errors = [];
|
|
5360
|
+
const rest = evaluate(doc, /* @__PURE__ */ new Map());
|
|
5361
|
+
cases.forEach((c, index) => {
|
|
5362
|
+
const skel = evaluate(doc, controlPose(doc, c.values));
|
|
5363
|
+
for (const [id, b] of skel.bones) if (Object.values(b.world).some((v) => !Number.isFinite(v)))
|
|
5364
|
+
errors.push({ case: index, subject: id, message: "non-finite bone transform" });
|
|
5365
|
+
for (const att of doc.attachments) if (att.type === "mesh") {
|
|
5366
|
+
const before = attachmentWorldVertices(rest, att.slot, att), after = attachmentWorldVertices(skel, att.slot, att);
|
|
5367
|
+
if (after.some((v) => !Number.isFinite(v))) errors.push({ case: index, subject: att.id, message: "non-finite mesh vertices" });
|
|
5368
|
+
else if (foldedTriangles(before, after, att.triangles).length || att.triangles.some((_, i) => i % 3 === 0 && Math.abs(triangleArea(before, ...att.triangles.slice(i, i + 3))) > 1e-8 && Math.abs(triangleArea(after, ...att.triangles.slice(i, i + 3))) < 1e-8))
|
|
5369
|
+
errors.push({ case: index, subject: att.id, message: "mesh triangle reversed or collapsed" });
|
|
5370
|
+
}
|
|
5371
|
+
});
|
|
5372
|
+
const neutral = JSON.stringify(snapshot(doc, {}));
|
|
5373
|
+
const effects = (doc.controls ?? []).map((c) => ({
|
|
5374
|
+
id: c.id,
|
|
5375
|
+
geometryOrOpacityChanged: [c.min, c.max].some((v) => JSON.stringify(snapshot(doc, { [c.id]: v })) !== neutral)
|
|
5376
|
+
}));
|
|
5377
|
+
return {
|
|
5378
|
+
passed: errors.length === 0,
|
|
5379
|
+
errors,
|
|
5380
|
+
effects,
|
|
5381
|
+
scope: "Finite transforms and mesh orientation at the listed poses; individual control geometry/opacity effects. Images require visual review; this is not an artwork acceptance result."
|
|
5382
|
+
};
|
|
5383
|
+
}
|
|
5384
|
+
function makeControlSweep(doc, probes) {
|
|
5385
|
+
const cases = controlCases(doc, probes).map((c) => ({ ...c, values: { ...Object.fromEntries((doc.controls ?? []).map((p) => [p.id, p.default])), ...c.values } }));
|
|
5386
|
+
const animation = {
|
|
5387
|
+
id: "control_sweep",
|
|
5388
|
+
duration: Math.max(1, cases.length - 1),
|
|
5389
|
+
loop: false,
|
|
5390
|
+
events: [],
|
|
5391
|
+
tracks: (doc.controls ?? []).map((c) => ({ target: "control", id: c.id, prop: "value", keys: cases.map((p, i) => ({ t: i, v: p.values[c.id], ease: "step" })) }))
|
|
5392
|
+
};
|
|
5393
|
+
return { cases, doc: parse({ ...doc, animations: [animation] }), animation: animation.id, times: cases.map((_, i) => i) };
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5210
5396
|
// src/rig-api.ts
|
|
5211
5397
|
var tools = [];
|
|
5212
5398
|
function tool(name, description, input, run) {
|
|
@@ -5269,7 +5455,80 @@ function previewCheck(doc, animation) {
|
|
|
5269
5455
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5270
5456
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5271
5457
|
}
|
|
5272
|
-
var TrackRef = { interpolation: z3.enum(["segment", "pchip"]).optional().describe("pchip: smooth through numeric keys, preserving extrema; segment: per-key easing"), target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder"]), id: z3.string(), prop: z3.string() };
|
|
5458
|
+
var TrackRef = { interpolation: z3.enum(["segment", "pchip"]).optional().describe("pchip: smooth through numeric keys, preserving extrema; segment: per-key easing"), target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder", "control"]), id: z3.string(), prop: z3.string() };
|
|
5459
|
+
tool(
|
|
5460
|
+
"rig_set_control",
|
|
5461
|
+
"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.",
|
|
5462
|
+
{ path: Path, control: RigControl },
|
|
5463
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5464
|
+
);
|
|
5465
|
+
tool(
|
|
5466
|
+
"rig_set_control_binding",
|
|
5467
|
+
"Create or replace a 1D/2D grid of absolute keyforms. First axis varies fastest. Channels support bone transforms, slot alpha and mesh/path vertex offsets. Each channel has one owner; animate controls rather than owned channels.",
|
|
5468
|
+
{ path: Path, binding: ControlBinding },
|
|
5469
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5470
|
+
);
|
|
5471
|
+
tool(
|
|
5472
|
+
"rig_remove_control_binding",
|
|
5473
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5474
|
+
{ path: Path, id: z3.string() },
|
|
5475
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5476
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5477
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5478
|
+
})
|
|
5479
|
+
);
|
|
5480
|
+
tool(
|
|
5481
|
+
"rig_remove_control",
|
|
5482
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5483
|
+
{ path: Path, id: z3.string() },
|
|
5484
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5485
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5486
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5487
|
+
return { ...d, controls: d.controls.filter((c) => c.id !== id), animations: d.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => t.target !== "control" || t.id !== id) })) };
|
|
5488
|
+
})
|
|
5489
|
+
);
|
|
5490
|
+
tool(
|
|
5491
|
+
"rig_sweep_controls",
|
|
5492
|
+
"Render an explicit control grid (max 256 poses) through PixiJS, with PNGs, contact sheet and report tying cases to source/texture/renderer hashes. Geometry checks are not visual acceptance. Use a new output directory outside the packed rig.",
|
|
5493
|
+
{
|
|
5494
|
+
path: Path,
|
|
5495
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5496
|
+
output: z3.string(),
|
|
5497
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5498
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5499
|
+
},
|
|
5500
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5501
|
+
try {
|
|
5502
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5503
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5504
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5505
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5506
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5507
|
+
await mkdir6(out, { recursive: true });
|
|
5508
|
+
const frames = [];
|
|
5509
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5510
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5511
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5512
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5513
|
+
}
|
|
5514
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5515
|
+
const report = {
|
|
5516
|
+
version: 1,
|
|
5517
|
+
sourceRig: sha256(raw),
|
|
5518
|
+
identity: r.identity,
|
|
5519
|
+
scale,
|
|
5520
|
+
region: region ?? null,
|
|
5521
|
+
...qa,
|
|
5522
|
+
frames,
|
|
5523
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5524
|
+
};
|
|
5525
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5526
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5527
|
+
} catch (e) {
|
|
5528
|
+
return fail(e);
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
);
|
|
5273
5532
|
var KeyIn = z3.object({
|
|
5274
5533
|
t: z3.number().describe("seconds"),
|
|
5275
5534
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
@@ -6101,8 +6360,8 @@ tool(
|
|
|
6101
6360
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6102
6361
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6103
6362
|
if (framesDir) {
|
|
6104
|
-
const { mkdir:
|
|
6105
|
-
await
|
|
6363
|
+
const { mkdir: mkdir8 } = await import("node:fs/promises");
|
|
6364
|
+
await mkdir8(framesDir, { recursive: true });
|
|
6106
6365
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6107
6366
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6108
6367
|
const manifest = {
|
|
@@ -6252,7 +6511,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6252
6511
|
async function listRigs() {
|
|
6253
6512
|
const out = [];
|
|
6254
6513
|
const walk = async (dir) => {
|
|
6255
|
-
for (const e of await
|
|
6514
|
+
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
6256
6515
|
if (e.isDirectory()) {
|
|
6257
6516
|
if (!SKIP.has(e.name)) await walk(join10(dir, e.name));
|
|
6258
6517
|
continue;
|
|
@@ -6272,7 +6531,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6272
6531
|
}
|
|
6273
6532
|
async function listFiles(rigAbs) {
|
|
6274
6533
|
const dir = dirname9(rigAbs), out = [];
|
|
6275
|
-
for (const e of (await
|
|
6534
|
+
for (const e of (await readdir5(dir, { withFileTypes: true })).filter((e2) => e2.isFile() && /\.png$/i.test(e2.name)).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6276
6535
|
const fh = await open(join10(dir, e.name));
|
|
6277
6536
|
try {
|
|
6278
6537
|
const head = Buffer.alloc(24);
|
|
@@ -6390,7 +6649,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6390
6649
|
const abs = inside(rig);
|
|
6391
6650
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6392
6651
|
if (await stat3(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
|
|
6393
|
-
await
|
|
6652
|
+
await mkdir7(dirname9(abs), { recursive: true });
|
|
6394
6653
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6395
6654
|
ensureWatch(abs);
|
|
6396
6655
|
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
|