@energy8platform/golem 0.3.0 → 0.6.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 +21 -0
- package/dist/editor.css +50 -0
- package/dist/editor.js +1384 -983
- package/dist/lib/cli.js +487 -83
- 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 +245 -1
- package/dist/lib/editor-entry.js +487 -83
- package/dist/lib/editor-entry.js.map +4 -4
- package/dist/lib/harness.js +236 -19
- 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 +7 -2
- package/dist/lib/rig-control-qa.d.ts +346 -0
- package/dist/lib/rig-controls.d.ts +13 -0
- package/dist/lib/rig-format.d.ts +1903 -2
- 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 +239 -19
- package/dist/lib/runtime.js.map +4 -4
- package/dist/lib/spine-import.d.ts +4 -0
- package/dist/lib/tools.js +483 -79
- 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
|
|
@@ -216,7 +216,9 @@ var PathAttachment = z.object({
|
|
|
216
216
|
/** curve lengths for constant-speed positioning */
|
|
217
217
|
lengths: z.array(z.number()).default([]),
|
|
218
218
|
closed: z.boolean().default(false),
|
|
219
|
-
constantSpeed: z.boolean().default(true)
|
|
219
|
+
constantSpeed: z.boolean().default(true),
|
|
220
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
221
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
220
222
|
});
|
|
221
223
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
222
224
|
var IkConstraint = z.object({
|
|
@@ -285,10 +287,13 @@ var Key = z.object({
|
|
|
285
287
|
t: z.number().nonnegative(),
|
|
286
288
|
v: KeyValue,
|
|
287
289
|
/** easing INTO the next key */
|
|
288
|
-
ease: Ease.default("ease_in_out")
|
|
290
|
+
ease: Ease.default("ease_in_out"),
|
|
291
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
292
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
289
293
|
});
|
|
290
294
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
291
295
|
var Track = z.discriminatedUnion("target", [
|
|
296
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
292
297
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
293
298
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
294
299
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -311,6 +316,25 @@ var Animation = z.object({
|
|
|
311
316
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
312
317
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
313
318
|
});
|
|
319
|
+
var RigControl = z.object({
|
|
320
|
+
id: Id,
|
|
321
|
+
label: z.string().optional(),
|
|
322
|
+
min: z.number().finite(),
|
|
323
|
+
max: z.number().finite(),
|
|
324
|
+
default: z.number().finite()
|
|
325
|
+
}).strict();
|
|
326
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
327
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
328
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
329
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
330
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
331
|
+
]);
|
|
332
|
+
var ControlBinding = z.object({
|
|
333
|
+
id: Id,
|
|
334
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
335
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
336
|
+
channels: z.array(ControlChannel).min(1)
|
|
337
|
+
}).strict();
|
|
314
338
|
var Reference = z.object({
|
|
315
339
|
src: z.string(),
|
|
316
340
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -336,7 +360,9 @@ var RigSchema = z.object({
|
|
|
336
360
|
ik: z.array(IkConstraint).default([]),
|
|
337
361
|
transform: z.array(TransformConstraint).default([]),
|
|
338
362
|
path: z.array(PathConstraint).default([]),
|
|
339
|
-
animations: z.array(Animation)
|
|
363
|
+
animations: z.array(Animation),
|
|
364
|
+
controls: z.array(RigControl).optional(),
|
|
365
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
340
366
|
});
|
|
341
367
|
var RigDocument = z.preprocess((input) => {
|
|
342
368
|
const v = input?.version;
|
|
@@ -411,6 +437,8 @@ function validate(doc) {
|
|
|
411
437
|
dup(doc.ik, "ik");
|
|
412
438
|
dup(doc.transform, "transform");
|
|
413
439
|
dup(doc.path, "path");
|
|
440
|
+
dup(doc.controls ?? [], "control");
|
|
441
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
414
442
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
415
443
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
416
444
|
const slots = new Set(doc.slots.map((s) => s.id));
|
|
@@ -448,6 +476,10 @@ function validate(doc) {
|
|
|
448
476
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
449
477
|
}
|
|
450
478
|
}
|
|
479
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
480
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
481
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
482
|
+
}
|
|
451
483
|
const vertexCount2 = (a) => {
|
|
452
484
|
if (a.type === "region") return 4;
|
|
453
485
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -485,20 +517,59 @@ function validate(doc) {
|
|
|
485
517
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
486
518
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
487
519
|
}
|
|
520
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
521
|
+
const driven = /* @__PURE__ */ new Set();
|
|
522
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
523
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
524
|
+
for (const b of doc.controlBindings ?? []) {
|
|
525
|
+
const prefix = `control binding "${b.id}"`;
|
|
526
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
527
|
+
for (const axis of b.axes) {
|
|
528
|
+
const c = controls.get(axis.control);
|
|
529
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
530
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
531
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
532
|
+
}
|
|
533
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
534
|
+
for (const ch of b.channels) {
|
|
535
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
536
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
537
|
+
driven.add(key);
|
|
538
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
539
|
+
if (ch.target === "deform") {
|
|
540
|
+
const att = attachments.get(ch.id);
|
|
541
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
542
|
+
else {
|
|
543
|
+
const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
|
|
544
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
548
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
549
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
488
553
|
for (const a of doc.animations) {
|
|
489
554
|
const tracks = /* @__PURE__ */ new Set();
|
|
490
555
|
for (const t of a.tracks) {
|
|
491
556
|
const tid = trackId(t);
|
|
492
557
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
493
558
|
tracks.add(tid);
|
|
494
|
-
|
|
559
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
560
|
+
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
561
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
496
562
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
497
563
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
498
564
|
let prev = -1;
|
|
499
565
|
for (const k of t.keys) {
|
|
566
|
+
if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
|
|
500
567
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
501
568
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
569
|
+
if (t.target === "control") {
|
|
570
|
+
const c = controls.get(t.id);
|
|
571
|
+
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`);
|
|
572
|
+
}
|
|
502
573
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
503
574
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
504
575
|
else if (typeof k.v === "string") {
|
|
@@ -537,6 +608,65 @@ function parse(input) {
|
|
|
537
608
|
return doc;
|
|
538
609
|
}
|
|
539
610
|
|
|
611
|
+
// src/rig-controls.ts
|
|
612
|
+
function controlPose(doc, values2) {
|
|
613
|
+
const out = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
615
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
616
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
617
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
618
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
function bracket(keys, value) {
|
|
623
|
+
let i = 0;
|
|
624
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
625
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
626
|
+
}
|
|
627
|
+
function sampleBinding(binding, values2) {
|
|
628
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
629
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
630
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
631
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
632
|
+
return binding.channels.map((ch) => {
|
|
633
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
634
|
+
const at = (j2) => {
|
|
635
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
636
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
637
|
+
};
|
|
638
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function resolveControls(doc, input) {
|
|
642
|
+
if (!doc.controlBindings?.length) return input;
|
|
643
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
644
|
+
for (const b of doc.controlBindings) {
|
|
645
|
+
const values2 = b.axes.map((axis) => {
|
|
646
|
+
const c = controls.get(axis.control);
|
|
647
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
648
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
649
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
650
|
+
});
|
|
651
|
+
const sampled2 = sampleBinding(b, values2);
|
|
652
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
function controlCases(doc, probes) {
|
|
657
|
+
let cases = [{ label: "default", values: {} }];
|
|
658
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
659
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
660
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
661
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
662
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
663
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
664
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
665
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
666
|
+
}
|
|
667
|
+
return cases;
|
|
668
|
+
}
|
|
669
|
+
|
|
540
670
|
// src/rig-anim.ts
|
|
541
671
|
var DEG2 = Math.PI / 180;
|
|
542
672
|
var named = {
|
|
@@ -611,8 +741,8 @@ function sample(track, t, loop = false) {
|
|
|
611
741
|
}
|
|
612
742
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
613
743
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
614
|
-
const
|
|
615
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
744
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
745
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
616
746
|
}
|
|
617
747
|
return a.v;
|
|
618
748
|
}
|
|
@@ -640,6 +770,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
640
770
|
}
|
|
641
771
|
function restValue(doc, key) {
|
|
642
772
|
const [target, id, prop] = key.split(":");
|
|
773
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
643
774
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
644
775
|
if (target === "slot") {
|
|
645
776
|
const s = doc.slots.find((s2) => s2.id === id);
|
|
@@ -703,6 +834,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
703
834
|
if (!parent) return local;
|
|
704
835
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
705
836
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
837
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
838
|
+
const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
|
|
839
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
840
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
841
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
842
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
843
|
+
}
|
|
706
844
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
707
845
|
}
|
|
708
846
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -735,18 +873,20 @@ var localOf = (doc, pose, b) => {
|
|
|
735
873
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
736
874
|
};
|
|
737
875
|
};
|
|
738
|
-
function updateSubtree(skel, root2) {
|
|
876
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
739
877
|
const doc = skel.doc;
|
|
740
878
|
const kids = /* @__PURE__ */ new Map();
|
|
741
879
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
742
|
-
const stack = [root2];
|
|
880
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
743
881
|
while (stack.length) {
|
|
744
882
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
745
883
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
746
884
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
747
885
|
}
|
|
748
886
|
}
|
|
749
|
-
function evaluate(doc, pose) {
|
|
887
|
+
function evaluate(doc, pose, modifiers) {
|
|
888
|
+
pose = resolveControls(doc, pose);
|
|
889
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
750
890
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
751
891
|
for (const b of boneOrder(doc)) {
|
|
752
892
|
const s = localOf(doc, pose, b);
|
|
@@ -980,7 +1120,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
980
1120
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
981
1121
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
982
1122
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
983
|
-
const
|
|
1123
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1124
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
984
1125
|
for (const id of c.bones) {
|
|
985
1126
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
986
1127
|
if (c.local) {
|
|
@@ -1142,6 +1283,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1142
1283
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1143
1284
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1144
1285
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1286
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1145
1287
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1146
1288
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1147
1289
|
let sum = 0;
|
|
@@ -1176,9 +1318,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1176
1318
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1177
1319
|
}
|
|
1178
1320
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1321
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1179
1322
|
for (let i = 0; i < bones.length; i++) {
|
|
1180
1323
|
const { b, st } = bones[i];
|
|
1181
|
-
let w = { ...
|
|
1324
|
+
let w = { ...inputWorlds[i] };
|
|
1182
1325
|
w.tx += (boneX - w.tx) * mixX;
|
|
1183
1326
|
w.ty += (boneY - w.ty) * mixY;
|
|
1184
1327
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1208,7 +1351,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1208
1351
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1209
1352
|
w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
|
|
1210
1353
|
}
|
|
1211
|
-
|
|
1354
|
+
solvedWorlds.set(b.id, w);
|
|
1355
|
+
}
|
|
1356
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1357
|
+
const world = solvedWorlds.get(b.id);
|
|
1358
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1212
1359
|
}
|
|
1213
1360
|
}
|
|
1214
1361
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1237,9 +1384,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1237
1384
|
case "onlyTranslation":
|
|
1238
1385
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1239
1386
|
case "noRotationOrReflection": {
|
|
1240
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1387
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1241
1388
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1242
|
-
|
|
1389
|
+
if (lenX * lenX <= 1e-4) {
|
|
1390
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1391
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1392
|
+
}
|
|
1393
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1394
|
+
return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
|
|
1243
1395
|
}
|
|
1244
1396
|
case "noScale":
|
|
1245
1397
|
case "noScaleOrReflection": {
|
|
@@ -1265,7 +1417,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1265
1417
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1266
1418
|
st.shearX = 0;
|
|
1267
1419
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1268
|
-
|
|
1420
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1421
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1422
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1423
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1424
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1425
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1426
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1427
|
+
}
|
|
1428
|
+
st.world = world;
|
|
1429
|
+
updateSubtree(skel, bone.id, false);
|
|
1269
1430
|
}
|
|
1270
1431
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1271
1432
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1972,6 +2133,9 @@ function updateBone(doc, id, patch) {
|
|
|
1972
2133
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1973
2134
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1974
2135
|
const cur = doc.bones[i];
|
|
2136
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2137
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2138
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1975
2139
|
const { parent, ...rest } = patch;
|
|
1976
2140
|
const next = RigSchema.shape.bones.element.parse({
|
|
1977
2141
|
...cur,
|
|
@@ -2004,6 +2168,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2004
2168
|
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
2169
|
}
|
|
2006
2170
|
function setSlot(doc, slot) {
|
|
2171
|
+
if (slot.alpha !== void 0) {
|
|
2172
|
+
for (const b of doc.controlBindings ?? [])
|
|
2173
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2174
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2175
|
+
}
|
|
2007
2176
|
const cur = doc.slots.find((s) => s.id === slot.id);
|
|
2008
2177
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2009
2178
|
const base = doc.slots.find((s) => s.id === slot.id);
|
|
@@ -2016,7 +2185,7 @@ function removeSlot(doc, id) {
|
|
|
2016
2185
|
return patchDrawOrder({
|
|
2017
2186
|
...doc,
|
|
2018
2187
|
slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
|
|
2019
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2188
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2020
2189
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2021
2190
|
}, (o) => o.filter((s) => s !== id));
|
|
2022
2191
|
}
|
|
@@ -2378,6 +2547,7 @@ function trackSummary(t) {
|
|
|
2378
2547
|
function rename2(doc, kind, id, to) {
|
|
2379
2548
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2380
2549
|
if (to === id) return doc;
|
|
2550
|
+
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
2551
|
const sub = (v) => v === id ? to : v;
|
|
2382
2552
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2383
2553
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2411,7 +2581,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2411
2581
|
return {
|
|
2412
2582
|
...doc,
|
|
2413
2583
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2414
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2584
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2415
2585
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2416
2586
|
};
|
|
2417
2587
|
}
|
|
@@ -2459,6 +2629,8 @@ function describeAttachment(doc, att) {
|
|
|
2459
2629
|
}
|
|
2460
2630
|
function describe(doc, opts = {}) {
|
|
2461
2631
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2632
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2633
|
+
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
2634
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2463
2635
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2464
2636
|
const place = (s) => {
|
|
@@ -3158,8 +3330,8 @@ function motionQuality(doc, anim, opts = {}) {
|
|
|
3158
3330
|
if (!skel.attachment.get(front) || !skel.attachment.get(back)) continue;
|
|
3159
3331
|
const visible = (id) => {
|
|
3160
3332
|
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;
|
|
3333
|
+
const color = skel.pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3334
|
+
return Number(skel.pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3163
3335
|
};
|
|
3164
3336
|
if (!visible(front) || !visible(back)) continue;
|
|
3165
3337
|
const order = pose.get(pk("drawOrder", "order", "order"));
|
|
@@ -3234,6 +3406,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3234
3406
|
});
|
|
3235
3407
|
const W = doc.meta.width, H = doc.meta.height;
|
|
3236
3408
|
const overflowOf = (pose, slotId, skel) => {
|
|
3409
|
+
pose = skel.pose;
|
|
3237
3410
|
const pts = slotOutline(doc, pose, slotId, 4, skel);
|
|
3238
3411
|
if (!pts) return void 0;
|
|
3239
3412
|
const slot = doc.slots.find((s) => s.id === slotId), alphaV = pose.get(`slot:${slotId}:alpha`);
|
|
@@ -3432,6 +3605,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3432
3605
|
}
|
|
3433
3606
|
}
|
|
3434
3607
|
const vertexKeyed = new Set(a.tracks.filter((t) => t.target === "deform" && t.prop === "vertices").map((t) => t.id));
|
|
3608
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels) if (ch.target === "deform") vertexKeyed.add(ch.id);
|
|
3435
3609
|
const deformMeshes = meshes.filter((att) => vertexKeyed.has(att.id));
|
|
3436
3610
|
const bonesMeshes = meshes.filter((att) => !vertexKeyed.has(att.id));
|
|
3437
3611
|
const meshFolds = /* @__PURE__ */ new Map();
|
|
@@ -3768,11 +3942,13 @@ function diffDocs(a, b) {
|
|
|
3768
3942
|
["attachments", "attachment"],
|
|
3769
3943
|
["ik", "ik"],
|
|
3770
3944
|
["transform", "transform"],
|
|
3771
|
-
["path", "path"]
|
|
3945
|
+
["path", "path"],
|
|
3946
|
+
["controls", "control"],
|
|
3947
|
+
["controlBindings", "control binding"]
|
|
3772
3948
|
];
|
|
3773
3949
|
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]));
|
|
3950
|
+
const ma = new Map((a[kind] ?? []).map((x) => [x.id, x]));
|
|
3951
|
+
const mb = new Map((b[kind] ?? []).map((x) => [x.id, x]));
|
|
3776
3952
|
for (const [id, x] of mb) if (!ma.has(id)) out.push(`+ ${name} ${id} ${j(x)}`);
|
|
3777
3953
|
for (const [id, x] of ma) {
|
|
3778
3954
|
const y = mb.get(id);
|
|
@@ -4018,6 +4194,9 @@ var Ids = class {
|
|
|
4018
4194
|
has(name) {
|
|
4019
4195
|
return this.map.has(name);
|
|
4020
4196
|
}
|
|
4197
|
+
reserve(id) {
|
|
4198
|
+
this.used.add(id);
|
|
4199
|
+
}
|
|
4021
4200
|
};
|
|
4022
4201
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4023
4202
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
@@ -4026,6 +4205,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4026
4205
|
const warnings = [];
|
|
4027
4206
|
const atlas = parseAtlas(atlasText);
|
|
4028
4207
|
const version = String(json.skeleton?.spine ?? "4.0");
|
|
4208
|
+
if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4029
4209
|
const major = parseFloat(version);
|
|
4030
4210
|
const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
|
|
4031
4211
|
const margin = opts.margin ?? 0;
|
|
@@ -4033,6 +4213,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4033
4213
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4034
4214
|
const boneIds = new Ids("b_"), slotIds = new Ids("s_"), assetIds = new Ids("a_"), attIds = new Ids("att_"), ikIds = new Ids("ik_"), tcIds = new Ids("tc_"), pcIds = new Ids("pc_"), animIds = new Ids("anim_");
|
|
4035
4215
|
const ROOT = "origin";
|
|
4216
|
+
boneIds.reserve(ROOT);
|
|
4036
4217
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4037
4218
|
const attachmentId = (slot, name) => {
|
|
4038
4219
|
const key = `${slot} ${name}`;
|
|
@@ -4057,13 +4238,17 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4057
4238
|
});
|
|
4058
4239
|
}
|
|
4059
4240
|
const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4241
|
+
const selectedSkin = opts.skin ? skins.find((s) => s.name === opts.skin) : skins.find((s) => s.name === "default") ?? skins[0];
|
|
4242
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s) => s.name).join(", ")}`);
|
|
4243
|
+
const lookupSkins = [selectedSkin, skins.find((s) => s.name === "default")].filter((s, i, list) => s && list.indexOf(s) === i);
|
|
4244
|
+
if (skins.length > 1) warnings.push(`Spine skin "${selectedSkin?.name}" selected; setup and attachment timelines are flattened to this skin with default fallback (no runtime skin switching)`);
|
|
4060
4245
|
const frameSuffix = (seq, index) => {
|
|
4061
|
-
const n = String((seq.start ??
|
|
4246
|
+
const n = String((seq.start ?? 1) + index);
|
|
4062
4247
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4063
4248
|
};
|
|
4064
4249
|
const sequences = /* @__PURE__ */ new Map();
|
|
4065
4250
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4066
|
-
for (const skin2 of
|
|
4251
|
+
for (const skin2 of lookupSkins) {
|
|
4067
4252
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4068
4253
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4069
4254
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4072,7 +4257,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4072
4257
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4073
4258
|
}
|
|
4074
4259
|
}
|
|
4075
|
-
const attachmentRef = (slot, name) =>
|
|
4260
|
+
const attachmentRef = (slot, name) => {
|
|
4261
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4262
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4263
|
+
};
|
|
4076
4264
|
const spineSlots = json.slots ?? [];
|
|
4077
4265
|
const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
|
|
4078
4266
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
@@ -4080,11 +4268,12 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4080
4268
|
id: slotIds.get(s.name),
|
|
4081
4269
|
bone: boneIds.get(s.bone),
|
|
4082
4270
|
z: i,
|
|
4083
|
-
...s.attachment ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4271
|
+
...s.attachment && attachmentRef(s.name, s.attachment) ? { attachment: attachmentRef(s.name, s.attachment) } : {},
|
|
4084
4272
|
blend: blendOf(s.blend),
|
|
4085
4273
|
color: hex8(s.color),
|
|
4086
4274
|
alpha: 1
|
|
4087
4275
|
}));
|
|
4276
|
+
if (spineSlots.some((s) => s.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4088
4277
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4089
4278
|
const assets = /* @__PURE__ */ new Map();
|
|
4090
4279
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4110,6 +4299,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4110
4299
|
return id;
|
|
4111
4300
|
};
|
|
4112
4301
|
const attachments = [];
|
|
4302
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4113
4303
|
const readVertices = (a, vertexCount2) => {
|
|
4114
4304
|
const v = a.vertices ?? [];
|
|
4115
4305
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4130,6 +4320,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4130
4320
|
}
|
|
4131
4321
|
return { weights };
|
|
4132
4322
|
};
|
|
4323
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4324
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4325
|
+
if (visiting.has(key)) {
|
|
4326
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4327
|
+
return void 0;
|
|
4328
|
+
}
|
|
4329
|
+
const raw = skins.find((s) => s.name === skinName)?.attachments?.[slot]?.[name];
|
|
4330
|
+
if (!raw) {
|
|
4331
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4332
|
+
return void 0;
|
|
4333
|
+
}
|
|
4334
|
+
if (!raw.parent) return raw;
|
|
4335
|
+
visiting.add(key);
|
|
4336
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4337
|
+
};
|
|
4338
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4133
4339
|
for (const skin2 of skins) {
|
|
4134
4340
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4135
4341
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4141,7 +4347,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4141
4347
|
for (const [name, a] of Object.entries(atts)) {
|
|
4142
4348
|
if (a.sequence) {
|
|
4143
4349
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4144
|
-
const base = a.path ?? name;
|
|
4350
|
+
const base = a.path ?? a.name ?? name;
|
|
4145
4351
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4146
4352
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4147
4353
|
if (!asset) continue;
|
|
@@ -4164,7 +4370,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4164
4370
|
}
|
|
4165
4371
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4166
4372
|
if (type === "region") {
|
|
4167
|
-
const asset = assetFor(a.path ?? name);
|
|
4373
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4168
4374
|
if (!asset) continue;
|
|
4169
4375
|
attachments.push({
|
|
4170
4376
|
id,
|
|
@@ -4180,21 +4386,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4180
4386
|
...a.height ? { height: a.height } : {}
|
|
4181
4387
|
});
|
|
4182
4388
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
}
|
|
4187
|
-
const asset = assetFor(a.path ?? name);
|
|
4389
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4390
|
+
if (!geometry) continue;
|
|
4391
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4188
4392
|
if (!asset) continue;
|
|
4189
|
-
const n = (
|
|
4190
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4393
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4394
|
+
attachments.push({ id, slot, type: "mesh", asset, uvs: geometry.uvs, triangles: geometry.triangles, ...readVertices(geometry, n), hull: geometry.hull ?? 0, ...geometry.width ? { width: geometry.width } : {}, ...geometry.height ? { height: geometry.height } : {} });
|
|
4395
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4396
|
+
const parentSkin = a.skin ?? "default";
|
|
4397
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4398
|
+
}
|
|
4191
4399
|
} else if (type === "path") {
|
|
4192
4400
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4401
|
+
} else if (type === "clipping") {
|
|
4402
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4403
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4404
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4405
|
+
[[i, 1]],
|
|
4406
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4407
|
+
]).flat();
|
|
4408
|
+
let geometry;
|
|
4409
|
+
if (data.weights) {
|
|
4410
|
+
const starts = [];
|
|
4411
|
+
let offset = 0;
|
|
4412
|
+
for (const list of data.weights) {
|
|
4413
|
+
starts.push(offset);
|
|
4414
|
+
offset += list.length * 2;
|
|
4415
|
+
}
|
|
4416
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4417
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4418
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4419
|
+
} else {
|
|
4420
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4421
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4422
|
+
clipDeforms.set(id, expand);
|
|
4423
|
+
}
|
|
4424
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4193
4425
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4194
4426
|
}
|
|
4195
4427
|
}
|
|
4196
4428
|
}
|
|
4197
|
-
const
|
|
4429
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s) => s[kind]?.includes(c.name));
|
|
4430
|
+
const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4198
4431
|
id: ikIds.get(c.name),
|
|
4199
4432
|
order: c.order ?? 0,
|
|
4200
4433
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4207,7 +4440,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4207
4440
|
stretch: !!c.stretch,
|
|
4208
4441
|
uniform: !!c.uniform
|
|
4209
4442
|
}));
|
|
4210
|
-
const transform = (json.transform ?? []).map((c) => ({
|
|
4443
|
+
const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4211
4444
|
id: tcIds.get(c.name),
|
|
4212
4445
|
order: c.order ?? 0,
|
|
4213
4446
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4227,7 +4460,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4227
4460
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4228
4461
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4229
4462
|
}));
|
|
4230
|
-
const path = (json.path ?? []).map((c) => ({
|
|
4463
|
+
const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4231
4464
|
id: pcIds.get(c.name),
|
|
4232
4465
|
order: c.order ?? 0,
|
|
4233
4466
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4262,7 +4495,21 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4262
4495
|
for (let i = 0; i < frames.length; i++) {
|
|
4263
4496
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4264
4497
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4265
|
-
|
|
4498
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4499
|
+
const value = f[field] ?? def;
|
|
4500
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4501
|
+
const cubic = (p0, p1, p2, p3, u) => (1 - u) ** 3 * p0 + 3 * (1 - u) ** 2 * u * p1 + 3 * (1 - u) * u * u * p2 + u ** 3 * p3;
|
|
4502
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4503
|
+
const u = (k + 1) / 10;
|
|
4504
|
+
return { t: r4(cubic(t, curve[0], curve[2], next.time ?? 0, u)), v: r4(abs(cubic(value, curve[1], curve[3], value, u))), ease: "linear" };
|
|
4505
|
+
});
|
|
4506
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4507
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4511
|
+
}
|
|
4512
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4266
4513
|
}
|
|
4267
4514
|
return keys;
|
|
4268
4515
|
};
|
|
@@ -4273,6 +4520,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4273
4520
|
const seen = (frames) => {
|
|
4274
4521
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4275
4522
|
};
|
|
4523
|
+
if (a.physics) {
|
|
4524
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4525
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4526
|
+
}
|
|
4276
4527
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4277
4528
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4278
4529
|
if (!sb) {
|
|
@@ -4313,19 +4564,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4313
4564
|
if (kind === "attachment") {
|
|
4314
4565
|
attachmentFrames.set(slotName, frames);
|
|
4315
4566
|
tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
|
|
4316
|
-
} else if (
|
|
4567
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4568
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4569
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4317
4570
|
const rgbaOf = (f) => {
|
|
4318
|
-
const c = hex8(f.color);
|
|
4319
|
-
return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4571
|
+
const c = hex8(f.color ?? f.light);
|
|
4572
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4320
4573
|
};
|
|
4574
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4321
4575
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4322
4576
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4323
|
-
return {
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4577
|
+
return {
|
|
4578
|
+
t: r4(f.time ?? 0),
|
|
4579
|
+
v: rgba.map(r4),
|
|
4580
|
+
ease: "linear",
|
|
4581
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4582
|
+
};
|
|
4329
4583
|
})));
|
|
4330
4584
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4331
4585
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4364,15 +4618,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4364
4618
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4365
4619
|
continue;
|
|
4366
4620
|
}
|
|
4367
|
-
const
|
|
4621
|
+
const rawClip = skins.find((s) => s.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4622
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4623
|
+
const data = clipData ?? att;
|
|
4624
|
+
const length = data.weights ? 2 * data.weights.reduce((s, l) => s + l.length, 0) : data.vertices?.length ?? 0;
|
|
4368
4625
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4369
4626
|
const full = new Array(length).fill(0);
|
|
4370
4627
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4371
4628
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4372
4629
|
const next = frames[i + 1];
|
|
4373
|
-
return { t: r4(f.time ?? 0), v: full.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4630
|
+
return { t: r4(f.time ?? 0), v: (clipDeforms.get(id)?.(full) ?? full).map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, 0, 1) : "linear" };
|
|
4374
4631
|
})));
|
|
4375
4632
|
}
|
|
4633
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4634
|
+
let changed = false;
|
|
4635
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4636
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4637
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4638
|
+
if (source) {
|
|
4639
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4640
|
+
changed = true;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
if (!changed) break;
|
|
4644
|
+
}
|
|
4376
4645
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4377
4646
|
if (Array.isArray(drawOrderFrames)) {
|
|
4378
4647
|
seen(drawOrderFrames);
|
|
@@ -4395,20 +4664,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4395
4664
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4396
4665
|
seen(frames);
|
|
4397
4666
|
const id = ikIds.get(name);
|
|
4667
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4398
4668
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4399
4669
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4400
4670
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4401
4671
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4402
4672
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4403
4673
|
}
|
|
4404
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4674
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4675
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4676
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4677
|
+
t: r4(f.time ?? 0),
|
|
4678
|
+
v: r4(value(f)),
|
|
4679
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4680
|
+
})));
|
|
4681
|
+
};
|
|
4409
4682
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4410
4683
|
seen(frames);
|
|
4411
4684
|
const id = tcIds.get(name);
|
|
4685
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4412
4686
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4413
4687
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4414
4688
|
}
|
|
@@ -4416,6 +4690,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4416
4690
|
const id = pcIds.get(name);
|
|
4417
4691
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4418
4692
|
seen(frames);
|
|
4693
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4419
4694
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4420
4695
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4421
4696
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4578,21 +4853,27 @@ async function importSpineProject(opts) {
|
|
|
4578
4853
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4579
4854
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4580
4855
|
const { pages } = parseAtlas(atlasText);
|
|
4581
|
-
if (pages.length
|
|
4582
|
-
|
|
4583
|
-
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
|
|
4856
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4857
|
+
if (opts.image && pages.length !== 1) throw new Error("image override requires a single-page atlas; multi-page atlases resolve each named page beside the atlas");
|
|
4584
4858
|
const rigDir = dirname6(opts.rigPath);
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4859
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4860
|
+
for (const page2 of pages) {
|
|
4861
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4862
|
+
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), page2.name), pageStem + ".png", pageStem + ".ktx2", ...pages.length === 1 ? [stem + ".png", stem + ".ktx2"] : []], `atlas page image ${page2.name}`);
|
|
4863
|
+
let pngPath = imagePath;
|
|
4864
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4865
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4866
|
+
if (!await exists(pngPath)) {
|
|
4867
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4868
|
+
await writeFile5(pngPath, png);
|
|
4869
|
+
}
|
|
4591
4870
|
}
|
|
4871
|
+
pagePaths.set(page2.name, pngPath);
|
|
4592
4872
|
}
|
|
4593
|
-
const pageImage = relative(rigDir,
|
|
4873
|
+
const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4594
4874
|
const name = opts.name ?? basename4(stem);
|
|
4595
|
-
|
|
4875
|
+
const imagePaths = [...pagePaths.values()];
|
|
4876
|
+
return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4596
4877
|
}
|
|
4597
4878
|
|
|
4598
4879
|
// src/rig-templates.ts
|
|
@@ -4609,7 +4890,7 @@ function extractTemplate(doc, animId, name, groups = {}) {
|
|
|
4609
4890
|
const tracks = [];
|
|
4610
4891
|
const skipped = [];
|
|
4611
4892
|
for (const t of anim.tracks) {
|
|
4612
|
-
if (t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4893
|
+
if (t.target === "control" || t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4613
4894
|
skipped.push(trackId5(t));
|
|
4614
4895
|
continue;
|
|
4615
4896
|
}
|
|
@@ -4984,8 +5265,8 @@ function runGates(doc, config) {
|
|
|
4984
5265
|
let active2 = 0, bad = 0;
|
|
4985
5266
|
for (const t of ts) {
|
|
4986
5267
|
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];
|
|
5268
|
+
const color = s.pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
5269
|
+
const alpha = Number(s.pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
4989
5270
|
if (!s.attachment.get(slot.id) || alpha <= 0.01) continue;
|
|
4990
5271
|
active2++;
|
|
4991
5272
|
const v = bone(s, rule.bone, rule.prop === "rotation" ? "rotation" : "position", rule.space)[rule.prop === "y" ? 1 : 0];
|
|
@@ -5125,13 +5406,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5125
5406
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5126
5407
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5127
5408
|
const id = `attach_${bone}_${target}`;
|
|
5409
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5128
5410
|
const offset = {
|
|
5129
5411
|
x,
|
|
5130
5412
|
y,
|
|
5131
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5413
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5132
5414
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5133
5415
|
scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
|
|
5134
|
-
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI)
|
|
5416
|
+
shearY: wrap2((Math.atan2(from.d, from.c) - Math.atan2(from.b, from.a) - Math.atan2(to.d, to.c) + Math.atan2(to.b, to.a)) * 180 / Math.PI) * handedness
|
|
5135
5417
|
};
|
|
5136
5418
|
const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
|
|
5137
5419
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5207,6 +5489,53 @@ function bakeSecondary(doc, animation, opts) {
|
|
|
5207
5489
|
return next;
|
|
5208
5490
|
}
|
|
5209
5491
|
|
|
5492
|
+
// src/rig-control-qa.ts
|
|
5493
|
+
function snapshot(doc, values2) {
|
|
5494
|
+
const skel = evaluate(doc, controlPose(doc, values2));
|
|
5495
|
+
return doc.slots.flatMap((slot) => {
|
|
5496
|
+
const alpha = Number(skel.pose.get(`slot:${slot.id}:alpha`) ?? slot.alpha);
|
|
5497
|
+
const points = slotOutline(doc, skel.pose, slot.id, 1, skel);
|
|
5498
|
+
return points ? [{ id: slot.id, alpha, points }] : [];
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
function checkControlCases(doc, cases) {
|
|
5502
|
+
const errors = [];
|
|
5503
|
+
const rest = evaluate(doc, /* @__PURE__ */ new Map());
|
|
5504
|
+
cases.forEach((c, index) => {
|
|
5505
|
+
const skel = evaluate(doc, controlPose(doc, c.values));
|
|
5506
|
+
for (const [id, b] of skel.bones) if (Object.values(b.world).some((v) => !Number.isFinite(v)))
|
|
5507
|
+
errors.push({ case: index, subject: id, message: "non-finite bone transform" });
|
|
5508
|
+
for (const att of doc.attachments) if (att.type === "mesh") {
|
|
5509
|
+
const before = attachmentWorldVertices(rest, att.slot, att), after = attachmentWorldVertices(skel, att.slot, att);
|
|
5510
|
+
if (after.some((v) => !Number.isFinite(v))) errors.push({ case: index, subject: att.id, message: "non-finite mesh vertices" });
|
|
5511
|
+
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))
|
|
5512
|
+
errors.push({ case: index, subject: att.id, message: "mesh triangle reversed or collapsed" });
|
|
5513
|
+
}
|
|
5514
|
+
});
|
|
5515
|
+
const neutral = JSON.stringify(snapshot(doc, {}));
|
|
5516
|
+
const effects = (doc.controls ?? []).map((c) => ({
|
|
5517
|
+
id: c.id,
|
|
5518
|
+
geometryOrOpacityChanged: [c.min, c.max].some((v) => JSON.stringify(snapshot(doc, { [c.id]: v })) !== neutral)
|
|
5519
|
+
}));
|
|
5520
|
+
return {
|
|
5521
|
+
passed: errors.length === 0,
|
|
5522
|
+
errors,
|
|
5523
|
+
effects,
|
|
5524
|
+
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."
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
function makeControlSweep(doc, probes) {
|
|
5528
|
+
const cases = controlCases(doc, probes).map((c) => ({ ...c, values: { ...Object.fromEntries((doc.controls ?? []).map((p) => [p.id, p.default])), ...c.values } }));
|
|
5529
|
+
const animation = {
|
|
5530
|
+
id: "control_sweep",
|
|
5531
|
+
duration: Math.max(1, cases.length - 1),
|
|
5532
|
+
loop: false,
|
|
5533
|
+
events: [],
|
|
5534
|
+
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" })) }))
|
|
5535
|
+
};
|
|
5536
|
+
return { cases, doc: parse({ ...doc, animations: [animation] }), animation: animation.id, times: cases.map((_, i) => i) };
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5210
5539
|
// src/rig-api.ts
|
|
5211
5540
|
var tools = [];
|
|
5212
5541
|
function tool(name, description, input, run) {
|
|
@@ -5270,14 +5599,88 @@ function previewCheck(doc, animation) {
|
|
|
5270
5599
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5271
5600
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5272
5601
|
}
|
|
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() };
|
|
5602
|
+
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() };
|
|
5603
|
+
tool(
|
|
5604
|
+
"rig_set_control",
|
|
5605
|
+
"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.",
|
|
5606
|
+
{ path: Path, control: RigControl },
|
|
5607
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5608
|
+
);
|
|
5609
|
+
tool(
|
|
5610
|
+
"rig_set_control_binding",
|
|
5611
|
+
"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.",
|
|
5612
|
+
{ path: Path, binding: ControlBinding },
|
|
5613
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5614
|
+
);
|
|
5615
|
+
tool(
|
|
5616
|
+
"rig_remove_control_binding",
|
|
5617
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5618
|
+
{ path: Path, id: z3.string() },
|
|
5619
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5620
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5621
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5622
|
+
})
|
|
5623
|
+
);
|
|
5624
|
+
tool(
|
|
5625
|
+
"rig_remove_control",
|
|
5626
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5627
|
+
{ path: Path, id: z3.string() },
|
|
5628
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5629
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5630
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5631
|
+
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) })) };
|
|
5632
|
+
})
|
|
5633
|
+
);
|
|
5634
|
+
tool(
|
|
5635
|
+
"rig_sweep_controls",
|
|
5636
|
+
"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.",
|
|
5637
|
+
{
|
|
5638
|
+
path: Path,
|
|
5639
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5640
|
+
output: z3.string(),
|
|
5641
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5642
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5643
|
+
},
|
|
5644
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5645
|
+
try {
|
|
5646
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5647
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5648
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5649
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5650
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5651
|
+
await mkdir6(out, { recursive: true });
|
|
5652
|
+
const frames = [];
|
|
5653
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5654
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5655
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5656
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5657
|
+
}
|
|
5658
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5659
|
+
const report = {
|
|
5660
|
+
version: 1,
|
|
5661
|
+
sourceRig: sha256(raw),
|
|
5662
|
+
identity: r.identity,
|
|
5663
|
+
scale,
|
|
5664
|
+
region: region ?? null,
|
|
5665
|
+
...qa,
|
|
5666
|
+
frames,
|
|
5667
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5668
|
+
};
|
|
5669
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5670
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5671
|
+
} catch (e) {
|
|
5672
|
+
return fail(e);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
);
|
|
5274
5676
|
var KeyIn = z3.object({
|
|
5275
5677
|
t: z3.number().describe("seconds"),
|
|
5276
5678
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5679
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5277
5680
|
ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
|
|
5278
5681
|
});
|
|
5279
5682
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5280
|
-
var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5683
|
+
var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
|
|
5281
5684
|
tool(
|
|
5282
5685
|
"rig_create",
|
|
5283
5686
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5401,19 +5804,20 @@ tool(
|
|
|
5401
5804
|
);
|
|
5402
5805
|
tool(
|
|
5403
5806
|
"rig_import_spine",
|
|
5404
|
-
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text;
|
|
5807
|
+
"Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
|
|
5405
5808
|
{
|
|
5406
5809
|
path: Path,
|
|
5407
5810
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5408
5811
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5409
5812
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5813
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5410
5814
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5411
5815
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5412
5816
|
notes: z3.string().optional()
|
|
5413
5817
|
},
|
|
5414
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5818
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5415
5819
|
try {
|
|
5416
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5820
|
+
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
|
|
5417
5821
|
const res = await create(path, parse(r.doc));
|
|
5418
5822
|
if (r.warnings.length) res.text += `
|
|
5419
5823
|
warnings:
|
|
@@ -6102,8 +6506,8 @@ tool(
|
|
|
6102
6506
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6103
6507
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6104
6508
|
if (framesDir) {
|
|
6105
|
-
const { mkdir:
|
|
6106
|
-
await
|
|
6509
|
+
const { mkdir: mkdir7 } = await import("node:fs/promises");
|
|
6510
|
+
await mkdir7(framesDir, { recursive: true });
|
|
6107
6511
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6108
6512
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6109
6513
|
const manifest = {
|