@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/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
- 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) : true;
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: mkdir6 } = await import("node:fs/promises");
6106
- await mkdir6(framesDir, { recursive: true });
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 = {