@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/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 stat3, readdir as
|
|
6
|
+
import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
7
7
|
import { watch as fsWatch } from "node:fs";
|
|
8
8
|
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -249,6 +249,7 @@ var Key = z.object({
|
|
|
249
249
|
});
|
|
250
250
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
251
251
|
var Track = z.discriminatedUnion("target", [
|
|
252
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
252
253
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
253
254
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
254
255
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -271,6 +272,25 @@ var Animation = z.object({
|
|
|
271
272
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
272
273
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
273
274
|
});
|
|
275
|
+
var RigControl = z.object({
|
|
276
|
+
id: Id,
|
|
277
|
+
label: z.string().optional(),
|
|
278
|
+
min: z.number().finite(),
|
|
279
|
+
max: z.number().finite(),
|
|
280
|
+
default: z.number().finite()
|
|
281
|
+
}).strict();
|
|
282
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
283
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
284
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
285
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
286
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
287
|
+
]);
|
|
288
|
+
var ControlBinding = z.object({
|
|
289
|
+
id: Id,
|
|
290
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
291
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
292
|
+
channels: z.array(ControlChannel).min(1)
|
|
293
|
+
}).strict();
|
|
274
294
|
var Reference = z.object({
|
|
275
295
|
src: z.string(),
|
|
276
296
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -296,7 +316,9 @@ var RigSchema = z.object({
|
|
|
296
316
|
ik: z.array(IkConstraint).default([]),
|
|
297
317
|
transform: z.array(TransformConstraint).default([]),
|
|
298
318
|
path: z.array(PathConstraint).default([]),
|
|
299
|
-
animations: z.array(Animation)
|
|
319
|
+
animations: z.array(Animation),
|
|
320
|
+
controls: z.array(RigControl).optional(),
|
|
321
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
300
322
|
});
|
|
301
323
|
var RigDocument = z.preprocess((input) => {
|
|
302
324
|
const v = input?.version;
|
|
@@ -371,6 +393,8 @@ function validate(doc) {
|
|
|
371
393
|
dup(doc.ik, "ik");
|
|
372
394
|
dup(doc.transform, "transform");
|
|
373
395
|
dup(doc.path, "path");
|
|
396
|
+
dup(doc.controls ?? [], "control");
|
|
397
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
374
398
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
375
399
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
376
400
|
const slots = new Set(doc.slots.map((s2) => s2.id));
|
|
@@ -445,13 +469,47 @@ function validate(doc) {
|
|
|
445
469
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
446
470
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
447
471
|
}
|
|
472
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
473
|
+
const driven = /* @__PURE__ */ new Set();
|
|
474
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
475
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
476
|
+
for (const b of doc.controlBindings ?? []) {
|
|
477
|
+
const prefix = `control binding "${b.id}"`;
|
|
478
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
479
|
+
for (const axis of b.axes) {
|
|
480
|
+
const c = controls.get(axis.control);
|
|
481
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
482
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
483
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
484
|
+
}
|
|
485
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
486
|
+
for (const ch of b.channels) {
|
|
487
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
488
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
489
|
+
driven.add(key);
|
|
490
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
491
|
+
if (ch.target === "deform") {
|
|
492
|
+
const att = attachments.get(ch.id);
|
|
493
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
494
|
+
else {
|
|
495
|
+
const n = att.weights ? att.weights.reduce((s2, a) => s2 + a.length * 2, 0) : att.vertices.length;
|
|
496
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
500
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
501
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
448
505
|
for (const a of doc.animations) {
|
|
449
506
|
const tracks = /* @__PURE__ */ new Set();
|
|
450
507
|
for (const t of a.tracks) {
|
|
451
508
|
const tid = trackId(t);
|
|
452
509
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
453
510
|
tracks.add(tid);
|
|
454
|
-
|
|
511
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
512
|
+
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;
|
|
455
513
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
456
514
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
457
515
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
@@ -459,6 +517,10 @@ function validate(doc) {
|
|
|
459
517
|
for (const k of t.keys) {
|
|
460
518
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
461
519
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
520
|
+
if (t.target === "control") {
|
|
521
|
+
const c = controls.get(t.id);
|
|
522
|
+
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`);
|
|
523
|
+
}
|
|
462
524
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
463
525
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
464
526
|
else if (typeof k.v === "string") {
|
|
@@ -499,7 +561,7 @@ function parse(input) {
|
|
|
499
561
|
|
|
500
562
|
// src/rig-api.ts
|
|
501
563
|
import { z as z3 } from "zod";
|
|
502
|
-
import { readFile as readFile9, writeFile as writeFile7, stat as stat2 } from "node:fs/promises";
|
|
564
|
+
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
503
565
|
import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
|
|
504
566
|
|
|
505
567
|
// src/rig-io.ts
|
|
@@ -547,6 +609,65 @@ async function withRigQueue(path, op) {
|
|
|
547
609
|
}
|
|
548
610
|
}
|
|
549
611
|
|
|
612
|
+
// src/rig-controls.ts
|
|
613
|
+
function controlPose(doc, values2) {
|
|
614
|
+
const out = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
616
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
617
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
618
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
619
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function bracket(keys, value) {
|
|
624
|
+
let i = 0;
|
|
625
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
626
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
627
|
+
}
|
|
628
|
+
function sampleBinding(binding, values2) {
|
|
629
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
630
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
631
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
632
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
633
|
+
return binding.channels.map((ch) => {
|
|
634
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
635
|
+
const at = (j2) => {
|
|
636
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
637
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
638
|
+
};
|
|
639
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function resolveControls(doc, input) {
|
|
643
|
+
if (!doc.controlBindings?.length) return input;
|
|
644
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
645
|
+
for (const b of doc.controlBindings) {
|
|
646
|
+
const values2 = b.axes.map((axis) => {
|
|
647
|
+
const c = controls.get(axis.control);
|
|
648
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
649
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
650
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
651
|
+
});
|
|
652
|
+
const sampled2 = sampleBinding(b, values2);
|
|
653
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function controlCases(doc, probes) {
|
|
658
|
+
let cases = [{ label: "default", values: {} }];
|
|
659
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
660
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
661
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
662
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
663
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
664
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
665
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
666
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
667
|
+
}
|
|
668
|
+
return cases;
|
|
669
|
+
}
|
|
670
|
+
|
|
550
671
|
// src/rig-anim.ts
|
|
551
672
|
var DEG2 = Math.PI / 180;
|
|
552
673
|
var named = {
|
|
@@ -650,6 +771,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
650
771
|
}
|
|
651
772
|
function restValue(doc, key) {
|
|
652
773
|
const [target, id, prop] = key.split(":");
|
|
774
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
653
775
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
654
776
|
if (target === "slot") {
|
|
655
777
|
const s2 = doc.slots.find((s3) => s3.id === id);
|
|
@@ -756,7 +878,9 @@ function updateSubtree(skel, root3) {
|
|
|
756
878
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
757
879
|
}
|
|
758
880
|
}
|
|
759
|
-
function evaluate(doc, pose) {
|
|
881
|
+
function evaluate(doc, pose, modifiers) {
|
|
882
|
+
pose = resolveControls(doc, pose);
|
|
883
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
760
884
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
761
885
|
for (const b of boneOrder(doc)) {
|
|
762
886
|
const s2 = localOf(doc, pose, b);
|
|
@@ -1982,6 +2106,9 @@ function updateBone(doc, id, patch) {
|
|
|
1982
2106
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1983
2107
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1984
2108
|
const cur = doc.bones[i];
|
|
2109
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2110
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2111
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1985
2112
|
const { parent, ...rest } = patch;
|
|
1986
2113
|
const next = RigSchema.shape.bones.element.parse({
|
|
1987
2114
|
...cur,
|
|
@@ -2014,6 +2141,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2014
2141
|
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) })) }) })) };
|
|
2015
2142
|
}
|
|
2016
2143
|
function setSlot(doc, slot) {
|
|
2144
|
+
if (slot.alpha !== void 0) {
|
|
2145
|
+
for (const b of doc.controlBindings ?? [])
|
|
2146
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2147
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2148
|
+
}
|
|
2017
2149
|
const cur = doc.slots.find((s2) => s2.id === slot.id);
|
|
2018
2150
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2019
2151
|
const base = doc.slots.find((s2) => s2.id === slot.id);
|
|
@@ -2388,6 +2520,7 @@ function trackSummary(t) {
|
|
|
2388
2520
|
function rename2(doc, kind, id, to) {
|
|
2389
2521
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2390
2522
|
if (to === id) return doc;
|
|
2523
|
+
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) })) };
|
|
2391
2524
|
const sub = (v) => v === id ? to : v;
|
|
2392
2525
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2393
2526
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2469,6 +2602,8 @@ function describeAttachment(doc, att) {
|
|
|
2469
2602
|
}
|
|
2470
2603
|
function describe(doc, opts = {}) {
|
|
2471
2604
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2605
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2606
|
+
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(", ")}`);
|
|
2472
2607
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2473
2608
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2474
2609
|
const place = (s2) => {
|
|
@@ -3168,8 +3303,8 @@ function motionQuality(doc, anim, opts = {}) {
|
|
|
3168
3303
|
if (!skel.attachment.get(front) || !skel.attachment.get(back)) continue;
|
|
3169
3304
|
const visible = (id) => {
|
|
3170
3305
|
const slot = doc.slots.find((s2) => s2.id === id);
|
|
3171
|
-
const color = pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3172
|
-
return Number(pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3306
|
+
const color = skel.pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3307
|
+
return Number(skel.pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3173
3308
|
};
|
|
3174
3309
|
if (!visible(front) || !visible(back)) continue;
|
|
3175
3310
|
const order = pose.get(pk("drawOrder", "order", "order"));
|
|
@@ -3244,6 +3379,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3244
3379
|
});
|
|
3245
3380
|
const W = doc.meta.width, H = doc.meta.height;
|
|
3246
3381
|
const overflowOf = (pose, slotId, skel) => {
|
|
3382
|
+
pose = skel.pose;
|
|
3247
3383
|
const pts = slotOutline(doc, pose, slotId, 4, skel);
|
|
3248
3384
|
if (!pts) return void 0;
|
|
3249
3385
|
const slot = doc.slots.find((s2) => s2.id === slotId), alphaV = pose.get(`slot:${slotId}:alpha`);
|
|
@@ -3442,6 +3578,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3442
3578
|
}
|
|
3443
3579
|
}
|
|
3444
3580
|
const vertexKeyed = new Set(a.tracks.filter((t) => t.target === "deform" && t.prop === "vertices").map((t) => t.id));
|
|
3581
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels) if (ch.target === "deform") vertexKeyed.add(ch.id);
|
|
3445
3582
|
const deformMeshes = meshes.filter((att) => vertexKeyed.has(att.id));
|
|
3446
3583
|
const bonesMeshes = meshes.filter((att) => !vertexKeyed.has(att.id));
|
|
3447
3584
|
const meshFolds = /* @__PURE__ */ new Map();
|
|
@@ -3778,11 +3915,13 @@ function diffDocs(a, b) {
|
|
|
3778
3915
|
["attachments", "attachment"],
|
|
3779
3916
|
["ik", "ik"],
|
|
3780
3917
|
["transform", "transform"],
|
|
3781
|
-
["path", "path"]
|
|
3918
|
+
["path", "path"],
|
|
3919
|
+
["controls", "control"],
|
|
3920
|
+
["controlBindings", "control binding"]
|
|
3782
3921
|
];
|
|
3783
3922
|
for (const [kind, name] of kinds) {
|
|
3784
|
-
const ma = new Map(a[kind].map((x) => [x.id, x]));
|
|
3785
|
-
const mb = new Map(b[kind].map((x) => [x.id, x]));
|
|
3923
|
+
const ma = new Map((a[kind] ?? []).map((x) => [x.id, x]));
|
|
3924
|
+
const mb = new Map((b[kind] ?? []).map((x) => [x.id, x]));
|
|
3786
3925
|
for (const [id, x] of mb) if (!ma.has(id)) out.push(`+ ${name} ${id} ${j(x)}`);
|
|
3787
3926
|
for (const [id, x] of ma) {
|
|
3788
3927
|
const y = mb.get(id);
|
|
@@ -4619,7 +4758,7 @@ function extractTemplate(doc, animId, name, groups = {}) {
|
|
|
4619
4758
|
const tracks = [];
|
|
4620
4759
|
const skipped = [];
|
|
4621
4760
|
for (const t of anim.tracks) {
|
|
4622
|
-
if (t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4761
|
+
if (t.target === "control" || t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4623
4762
|
skipped.push(trackId5(t));
|
|
4624
4763
|
continue;
|
|
4625
4764
|
}
|
|
@@ -4994,8 +5133,8 @@ function runGates(doc, config) {
|
|
|
4994
5133
|
let active2 = 0, bad = 0;
|
|
4995
5134
|
for (const t of ts) {
|
|
4996
5135
|
const pose = poseOf(animation, t), s2 = evaluate(doc, pose);
|
|
4997
|
-
const color = pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
4998
|
-
const alpha = Number(pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
5136
|
+
const color = s2.pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
5137
|
+
const alpha = Number(s2.pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
4999
5138
|
if (!s2.attachment.get(slot.id) || alpha <= 0.01) continue;
|
|
5000
5139
|
active2++;
|
|
5001
5140
|
const v = bone(s2, rule.bone, rule.prop === "rotation" ? "rotation" : "position", rule.space)[rule.prop === "y" ? 1 : 0];
|
|
@@ -5217,6 +5356,53 @@ function bakeSecondary(doc, animation, opts) {
|
|
|
5217
5356
|
return next;
|
|
5218
5357
|
}
|
|
5219
5358
|
|
|
5359
|
+
// src/rig-control-qa.ts
|
|
5360
|
+
function snapshot(doc, values2) {
|
|
5361
|
+
const skel = evaluate(doc, controlPose(doc, values2));
|
|
5362
|
+
return doc.slots.flatMap((slot) => {
|
|
5363
|
+
const alpha = Number(skel.pose.get(`slot:${slot.id}:alpha`) ?? slot.alpha);
|
|
5364
|
+
const points = slotOutline(doc, skel.pose, slot.id, 1, skel);
|
|
5365
|
+
return points ? [{ id: slot.id, alpha, points }] : [];
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5368
|
+
function checkControlCases(doc, cases) {
|
|
5369
|
+
const errors = [];
|
|
5370
|
+
const rest = evaluate(doc, /* @__PURE__ */ new Map());
|
|
5371
|
+
cases.forEach((c, index) => {
|
|
5372
|
+
const skel = evaluate(doc, controlPose(doc, c.values));
|
|
5373
|
+
for (const [id, b] of skel.bones) if (Object.values(b.world).some((v) => !Number.isFinite(v)))
|
|
5374
|
+
errors.push({ case: index, subject: id, message: "non-finite bone transform" });
|
|
5375
|
+
for (const att of doc.attachments) if (att.type === "mesh") {
|
|
5376
|
+
const before = attachmentWorldVertices(rest, att.slot, att), after = attachmentWorldVertices(skel, att.slot, att);
|
|
5377
|
+
if (after.some((v) => !Number.isFinite(v))) errors.push({ case: index, subject: att.id, message: "non-finite mesh vertices" });
|
|
5378
|
+
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))
|
|
5379
|
+
errors.push({ case: index, subject: att.id, message: "mesh triangle reversed or collapsed" });
|
|
5380
|
+
}
|
|
5381
|
+
});
|
|
5382
|
+
const neutral = JSON.stringify(snapshot(doc, {}));
|
|
5383
|
+
const effects = (doc.controls ?? []).map((c) => ({
|
|
5384
|
+
id: c.id,
|
|
5385
|
+
geometryOrOpacityChanged: [c.min, c.max].some((v) => JSON.stringify(snapshot(doc, { [c.id]: v })) !== neutral)
|
|
5386
|
+
}));
|
|
5387
|
+
return {
|
|
5388
|
+
passed: errors.length === 0,
|
|
5389
|
+
errors,
|
|
5390
|
+
effects,
|
|
5391
|
+
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."
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
5394
|
+
function makeControlSweep(doc, probes) {
|
|
5395
|
+
const cases = controlCases(doc, probes).map((c) => ({ ...c, values: { ...Object.fromEntries((doc.controls ?? []).map((p) => [p.id, p.default])), ...c.values } }));
|
|
5396
|
+
const animation = {
|
|
5397
|
+
id: "control_sweep",
|
|
5398
|
+
duration: Math.max(1, cases.length - 1),
|
|
5399
|
+
loop: false,
|
|
5400
|
+
events: [],
|
|
5401
|
+
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" })) }))
|
|
5402
|
+
};
|
|
5403
|
+
return { cases, doc: parse({ ...doc, animations: [animation] }), animation: animation.id, times: cases.map((_, i) => i) };
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5220
5406
|
// src/rig-api.ts
|
|
5221
5407
|
var tools = [];
|
|
5222
5408
|
function tool(name, description, input, run) {
|
|
@@ -5280,7 +5466,80 @@ function previewCheck(doc, animation) {
|
|
|
5280
5466
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5281
5467
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5282
5468
|
}
|
|
5283
|
-
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() };
|
|
5469
|
+
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() };
|
|
5470
|
+
tool(
|
|
5471
|
+
"rig_set_control",
|
|
5472
|
+
"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.",
|
|
5473
|
+
{ path: Path, control: RigControl },
|
|
5474
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5475
|
+
);
|
|
5476
|
+
tool(
|
|
5477
|
+
"rig_set_control_binding",
|
|
5478
|
+
"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.",
|
|
5479
|
+
{ path: Path, binding: ControlBinding },
|
|
5480
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5481
|
+
);
|
|
5482
|
+
tool(
|
|
5483
|
+
"rig_remove_control_binding",
|
|
5484
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5485
|
+
{ path: Path, id: z3.string() },
|
|
5486
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5487
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5488
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5489
|
+
})
|
|
5490
|
+
);
|
|
5491
|
+
tool(
|
|
5492
|
+
"rig_remove_control",
|
|
5493
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5494
|
+
{ path: Path, id: z3.string() },
|
|
5495
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5496
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5497
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5498
|
+
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) })) };
|
|
5499
|
+
})
|
|
5500
|
+
);
|
|
5501
|
+
tool(
|
|
5502
|
+
"rig_sweep_controls",
|
|
5503
|
+
"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.",
|
|
5504
|
+
{
|
|
5505
|
+
path: Path,
|
|
5506
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5507
|
+
output: z3.string(),
|
|
5508
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5509
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5510
|
+
},
|
|
5511
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5512
|
+
try {
|
|
5513
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5514
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5515
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5516
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5517
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5518
|
+
await mkdir6(out, { recursive: true });
|
|
5519
|
+
const frames = [];
|
|
5520
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5521
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5522
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5523
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5524
|
+
}
|
|
5525
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5526
|
+
const report = {
|
|
5527
|
+
version: 1,
|
|
5528
|
+
sourceRig: sha256(raw),
|
|
5529
|
+
identity: r.identity,
|
|
5530
|
+
scale,
|
|
5531
|
+
region: region ?? null,
|
|
5532
|
+
...qa,
|
|
5533
|
+
frames,
|
|
5534
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5535
|
+
};
|
|
5536
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5537
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5538
|
+
} catch (e) {
|
|
5539
|
+
return fail(e);
|
|
5540
|
+
}
|
|
5541
|
+
}
|
|
5542
|
+
);
|
|
5284
5543
|
var KeyIn = z3.object({
|
|
5285
5544
|
t: z3.number().describe("seconds"),
|
|
5286
5545
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
@@ -6112,8 +6371,8 @@ tool(
|
|
|
6112
6371
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6113
6372
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6114
6373
|
if (framesDir) {
|
|
6115
|
-
const { mkdir:
|
|
6116
|
-
await
|
|
6374
|
+
const { mkdir: mkdir8 } = await import("node:fs/promises");
|
|
6375
|
+
await mkdir8(framesDir, { recursive: true });
|
|
6117
6376
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6118
6377
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6119
6378
|
const manifest = {
|
|
@@ -6263,7 +6522,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6263
6522
|
async function listRigs() {
|
|
6264
6523
|
const out = [];
|
|
6265
6524
|
const walk = async (dir) => {
|
|
6266
|
-
for (const e of await
|
|
6525
|
+
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
6267
6526
|
if (e.isDirectory()) {
|
|
6268
6527
|
if (!SKIP.has(e.name)) await walk(join10(dir, e.name));
|
|
6269
6528
|
continue;
|
|
@@ -6283,7 +6542,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6283
6542
|
}
|
|
6284
6543
|
async function listFiles(rigAbs) {
|
|
6285
6544
|
const dir = dirname9(rigAbs), out = [];
|
|
6286
|
-
for (const e of (await
|
|
6545
|
+
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))) {
|
|
6287
6546
|
const fh = await open(join10(dir, e.name));
|
|
6288
6547
|
try {
|
|
6289
6548
|
const head = Buffer.alloc(24);
|
|
@@ -6401,7 +6660,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6401
6660
|
const abs = inside(rig);
|
|
6402
6661
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6403
6662
|
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` });
|
|
6404
|
-
await
|
|
6663
|
+
await mkdir7(dirname9(abs), { recursive: true });
|
|
6405
6664
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6406
6665
|
ensureWatch(abs);
|
|
6407
6666
|
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
|