@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/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/rig-api.ts
|
|
2
2
|
import { z as z3 } from "zod";
|
|
3
|
-
import { readFile as readFile9, writeFile as writeFile7, stat as stat2 } from "node:fs/promises";
|
|
3
|
+
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
4
4
|
import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/rig-io.ts
|
|
@@ -289,6 +289,7 @@ var Key = z.object({
|
|
|
289
289
|
});
|
|
290
290
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
291
291
|
var Track = z.discriminatedUnion("target", [
|
|
292
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
292
293
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
293
294
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
294
295
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -311,6 +312,25 @@ var Animation = z.object({
|
|
|
311
312
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
312
313
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
313
314
|
});
|
|
315
|
+
var RigControl = z.object({
|
|
316
|
+
id: Id,
|
|
317
|
+
label: z.string().optional(),
|
|
318
|
+
min: z.number().finite(),
|
|
319
|
+
max: z.number().finite(),
|
|
320
|
+
default: z.number().finite()
|
|
321
|
+
}).strict();
|
|
322
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
323
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
324
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
325
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
326
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
327
|
+
]);
|
|
328
|
+
var ControlBinding = z.object({
|
|
329
|
+
id: Id,
|
|
330
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
331
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
332
|
+
channels: z.array(ControlChannel).min(1)
|
|
333
|
+
}).strict();
|
|
314
334
|
var Reference = z.object({
|
|
315
335
|
src: z.string(),
|
|
316
336
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -336,7 +356,9 @@ var RigSchema = z.object({
|
|
|
336
356
|
ik: z.array(IkConstraint).default([]),
|
|
337
357
|
transform: z.array(TransformConstraint).default([]),
|
|
338
358
|
path: z.array(PathConstraint).default([]),
|
|
339
|
-
animations: z.array(Animation)
|
|
359
|
+
animations: z.array(Animation),
|
|
360
|
+
controls: z.array(RigControl).optional(),
|
|
361
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
340
362
|
});
|
|
341
363
|
var RigDocument = z.preprocess((input) => {
|
|
342
364
|
const v = input?.version;
|
|
@@ -411,6 +433,8 @@ function validate(doc) {
|
|
|
411
433
|
dup(doc.ik, "ik");
|
|
412
434
|
dup(doc.transform, "transform");
|
|
413
435
|
dup(doc.path, "path");
|
|
436
|
+
dup(doc.controls ?? [], "control");
|
|
437
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
414
438
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
415
439
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
416
440
|
const slots = new Set(doc.slots.map((s) => s.id));
|
|
@@ -485,13 +509,47 @@ function validate(doc) {
|
|
|
485
509
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
486
510
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
487
511
|
}
|
|
512
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
513
|
+
const driven = /* @__PURE__ */ new Set();
|
|
514
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
515
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
516
|
+
for (const b of doc.controlBindings ?? []) {
|
|
517
|
+
const prefix = `control binding "${b.id}"`;
|
|
518
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
519
|
+
for (const axis of b.axes) {
|
|
520
|
+
const c = controls.get(axis.control);
|
|
521
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
522
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
523
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
524
|
+
}
|
|
525
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
526
|
+
for (const ch of b.channels) {
|
|
527
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
528
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
529
|
+
driven.add(key);
|
|
530
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
531
|
+
if (ch.target === "deform") {
|
|
532
|
+
const att = attachments.get(ch.id);
|
|
533
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
534
|
+
else {
|
|
535
|
+
const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
|
|
536
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
540
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
541
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
488
545
|
for (const a of doc.animations) {
|
|
489
546
|
const tracks = /* @__PURE__ */ new Set();
|
|
490
547
|
for (const t of a.tracks) {
|
|
491
548
|
const tid = trackId(t);
|
|
492
549
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
493
550
|
tracks.add(tid);
|
|
494
|
-
|
|
551
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
552
|
+
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;
|
|
495
553
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
496
554
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
497
555
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
@@ -499,6 +557,10 @@ function validate(doc) {
|
|
|
499
557
|
for (const k of t.keys) {
|
|
500
558
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
501
559
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
560
|
+
if (t.target === "control") {
|
|
561
|
+
const c = controls.get(t.id);
|
|
562
|
+
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`);
|
|
563
|
+
}
|
|
502
564
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
503
565
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
504
566
|
else if (typeof k.v === "string") {
|
|
@@ -537,6 +599,65 @@ function parse(input) {
|
|
|
537
599
|
return doc;
|
|
538
600
|
}
|
|
539
601
|
|
|
602
|
+
// src/rig-controls.ts
|
|
603
|
+
function controlPose(doc, values2) {
|
|
604
|
+
const out = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
606
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
607
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
608
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
609
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
function bracket(keys, value) {
|
|
614
|
+
let i = 0;
|
|
615
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
616
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
617
|
+
}
|
|
618
|
+
function sampleBinding(binding, values2) {
|
|
619
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
620
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
621
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
622
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
623
|
+
return binding.channels.map((ch) => {
|
|
624
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
625
|
+
const at = (j2) => {
|
|
626
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
627
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
628
|
+
};
|
|
629
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function resolveControls(doc, input) {
|
|
633
|
+
if (!doc.controlBindings?.length) return input;
|
|
634
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
635
|
+
for (const b of doc.controlBindings) {
|
|
636
|
+
const values2 = b.axes.map((axis) => {
|
|
637
|
+
const c = controls.get(axis.control);
|
|
638
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
639
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
640
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
641
|
+
});
|
|
642
|
+
const sampled2 = sampleBinding(b, values2);
|
|
643
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
function controlCases(doc, probes) {
|
|
648
|
+
let cases = [{ label: "default", values: {} }];
|
|
649
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
650
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
651
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
652
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
653
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
654
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
655
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
656
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
657
|
+
}
|
|
658
|
+
return cases;
|
|
659
|
+
}
|
|
660
|
+
|
|
540
661
|
// src/rig-anim.ts
|
|
541
662
|
var DEG2 = Math.PI / 180;
|
|
542
663
|
var named = {
|
|
@@ -640,6 +761,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
640
761
|
}
|
|
641
762
|
function restValue(doc, key) {
|
|
642
763
|
const [target, id, prop] = key.split(":");
|
|
764
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
643
765
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
644
766
|
if (target === "slot") {
|
|
645
767
|
const s = doc.slots.find((s2) => s2.id === id);
|
|
@@ -746,7 +868,9 @@ function updateSubtree(skel, root2) {
|
|
|
746
868
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
747
869
|
}
|
|
748
870
|
}
|
|
749
|
-
function evaluate(doc, pose) {
|
|
871
|
+
function evaluate(doc, pose, modifiers) {
|
|
872
|
+
pose = resolveControls(doc, pose);
|
|
873
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
750
874
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
751
875
|
for (const b of boneOrder(doc)) {
|
|
752
876
|
const s = localOf(doc, pose, b);
|
|
@@ -1972,6 +2096,9 @@ function updateBone(doc, id, patch) {
|
|
|
1972
2096
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1973
2097
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1974
2098
|
const cur = doc.bones[i];
|
|
2099
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2100
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2101
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1975
2102
|
const { parent, ...rest } = patch;
|
|
1976
2103
|
const next = RigSchema.shape.bones.element.parse({
|
|
1977
2104
|
...cur,
|
|
@@ -2004,6 +2131,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2004
2131
|
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) })) }) })) };
|
|
2005
2132
|
}
|
|
2006
2133
|
function setSlot(doc, slot) {
|
|
2134
|
+
if (slot.alpha !== void 0) {
|
|
2135
|
+
for (const b of doc.controlBindings ?? [])
|
|
2136
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2137
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2138
|
+
}
|
|
2007
2139
|
const cur = doc.slots.find((s) => s.id === slot.id);
|
|
2008
2140
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2009
2141
|
const base = doc.slots.find((s) => s.id === slot.id);
|
|
@@ -2378,6 +2510,7 @@ function trackSummary(t) {
|
|
|
2378
2510
|
function rename2(doc, kind, id, to) {
|
|
2379
2511
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2380
2512
|
if (to === id) return doc;
|
|
2513
|
+
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) })) };
|
|
2381
2514
|
const sub = (v) => v === id ? to : v;
|
|
2382
2515
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2383
2516
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2459,6 +2592,8 @@ function describeAttachment(doc, att) {
|
|
|
2459
2592
|
}
|
|
2460
2593
|
function describe(doc, opts = {}) {
|
|
2461
2594
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2595
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2596
|
+
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(", ")}`);
|
|
2462
2597
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2463
2598
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2464
2599
|
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) {
|
|
@@ -5270,7 +5456,80 @@ function previewCheck(doc, animation) {
|
|
|
5270
5456
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5271
5457
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5272
5458
|
}
|
|
5273
|
-
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() };
|
|
5459
|
+
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() };
|
|
5460
|
+
tool(
|
|
5461
|
+
"rig_set_control",
|
|
5462
|
+
"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.",
|
|
5463
|
+
{ path: Path, control: RigControl },
|
|
5464
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5465
|
+
);
|
|
5466
|
+
tool(
|
|
5467
|
+
"rig_set_control_binding",
|
|
5468
|
+
"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.",
|
|
5469
|
+
{ path: Path, binding: ControlBinding },
|
|
5470
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5471
|
+
);
|
|
5472
|
+
tool(
|
|
5473
|
+
"rig_remove_control_binding",
|
|
5474
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5475
|
+
{ path: Path, id: z3.string() },
|
|
5476
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5477
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5478
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5479
|
+
})
|
|
5480
|
+
);
|
|
5481
|
+
tool(
|
|
5482
|
+
"rig_remove_control",
|
|
5483
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5484
|
+
{ path: Path, id: z3.string() },
|
|
5485
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5486
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5487
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5488
|
+
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) })) };
|
|
5489
|
+
})
|
|
5490
|
+
);
|
|
5491
|
+
tool(
|
|
5492
|
+
"rig_sweep_controls",
|
|
5493
|
+
"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.",
|
|
5494
|
+
{
|
|
5495
|
+
path: Path,
|
|
5496
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5497
|
+
output: z3.string(),
|
|
5498
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5499
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5500
|
+
},
|
|
5501
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5502
|
+
try {
|
|
5503
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5504
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5505
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5506
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5507
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5508
|
+
await mkdir6(out, { recursive: true });
|
|
5509
|
+
const frames = [];
|
|
5510
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5511
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5512
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5513
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5514
|
+
}
|
|
5515
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5516
|
+
const report = {
|
|
5517
|
+
version: 1,
|
|
5518
|
+
sourceRig: sha256(raw),
|
|
5519
|
+
identity: r.identity,
|
|
5520
|
+
scale,
|
|
5521
|
+
region: region ?? null,
|
|
5522
|
+
...qa,
|
|
5523
|
+
frames,
|
|
5524
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5525
|
+
};
|
|
5526
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5527
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5528
|
+
} catch (e) {
|
|
5529
|
+
return fail(e);
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
);
|
|
5274
5533
|
var KeyIn = z3.object({
|
|
5275
5534
|
t: z3.number().describe("seconds"),
|
|
5276
5535
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
@@ -6102,8 +6361,8 @@ tool(
|
|
|
6102
6361
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6103
6362
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6104
6363
|
if (framesDir) {
|
|
6105
|
-
const { mkdir:
|
|
6106
|
-
await
|
|
6364
|
+
const { mkdir: mkdir7 } = await import("node:fs/promises");
|
|
6365
|
+
await mkdir7(framesDir, { recursive: true });
|
|
6107
6366
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6108
6367
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6109
6368
|
const manifest = {
|