@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/editor-entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/editor/server.ts
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { readFile as readFile10, stat as stat3, readdir as
|
|
3
|
+
import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
4
4
|
import { watch as fsWatch } from "node:fs";
|
|
5
5
|
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -173,7 +173,9 @@ var PathAttachment = z.object({
|
|
|
173
173
|
/** curve lengths for constant-speed positioning */
|
|
174
174
|
lengths: z.array(z.number()).default([]),
|
|
175
175
|
closed: z.boolean().default(false),
|
|
176
|
-
constantSpeed: z.boolean().default(true)
|
|
176
|
+
constantSpeed: z.boolean().default(true),
|
|
177
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
178
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
177
179
|
});
|
|
178
180
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
179
181
|
var IkConstraint = z.object({
|
|
@@ -242,10 +244,13 @@ var Key = z.object({
|
|
|
242
244
|
t: z.number().nonnegative(),
|
|
243
245
|
v: KeyValue,
|
|
244
246
|
/** easing INTO the next key */
|
|
245
|
-
ease: Ease.default("ease_in_out")
|
|
247
|
+
ease: Ease.default("ease_in_out"),
|
|
248
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
249
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
246
250
|
});
|
|
247
251
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
248
252
|
var Track = z.discriminatedUnion("target", [
|
|
253
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
249
254
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
250
255
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
251
256
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -268,6 +273,25 @@ var Animation = z.object({
|
|
|
268
273
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
269
274
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
270
275
|
});
|
|
276
|
+
var RigControl = z.object({
|
|
277
|
+
id: Id,
|
|
278
|
+
label: z.string().optional(),
|
|
279
|
+
min: z.number().finite(),
|
|
280
|
+
max: z.number().finite(),
|
|
281
|
+
default: z.number().finite()
|
|
282
|
+
}).strict();
|
|
283
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
284
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
285
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
286
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
287
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
288
|
+
]);
|
|
289
|
+
var ControlBinding = z.object({
|
|
290
|
+
id: Id,
|
|
291
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
292
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
293
|
+
channels: z.array(ControlChannel).min(1)
|
|
294
|
+
}).strict();
|
|
271
295
|
var Reference = z.object({
|
|
272
296
|
src: z.string(),
|
|
273
297
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -293,7 +317,9 @@ var RigSchema = z.object({
|
|
|
293
317
|
ik: z.array(IkConstraint).default([]),
|
|
294
318
|
transform: z.array(TransformConstraint).default([]),
|
|
295
319
|
path: z.array(PathConstraint).default([]),
|
|
296
|
-
animations: z.array(Animation)
|
|
320
|
+
animations: z.array(Animation),
|
|
321
|
+
controls: z.array(RigControl).optional(),
|
|
322
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
297
323
|
});
|
|
298
324
|
var RigDocument = z.preprocess((input) => {
|
|
299
325
|
const v = input?.version;
|
|
@@ -368,6 +394,8 @@ function validate(doc) {
|
|
|
368
394
|
dup(doc.ik, "ik");
|
|
369
395
|
dup(doc.transform, "transform");
|
|
370
396
|
dup(doc.path, "path");
|
|
397
|
+
dup(doc.controls ?? [], "control");
|
|
398
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
371
399
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
372
400
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
373
401
|
const slots = new Set(doc.slots.map((s) => s.id));
|
|
@@ -405,6 +433,10 @@ function validate(doc) {
|
|
|
405
433
|
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
406
434
|
}
|
|
407
435
|
}
|
|
436
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
437
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
438
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
439
|
+
}
|
|
408
440
|
const vertexCount2 = (a) => {
|
|
409
441
|
if (a.type === "region") return 4;
|
|
410
442
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -442,20 +474,59 @@ function validate(doc) {
|
|
|
442
474
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
443
475
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
444
476
|
}
|
|
477
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
478
|
+
const driven = /* @__PURE__ */ new Set();
|
|
479
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
480
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
481
|
+
for (const b of doc.controlBindings ?? []) {
|
|
482
|
+
const prefix = `control binding "${b.id}"`;
|
|
483
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
484
|
+
for (const axis of b.axes) {
|
|
485
|
+
const c = controls.get(axis.control);
|
|
486
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
487
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
488
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
489
|
+
}
|
|
490
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
491
|
+
for (const ch of b.channels) {
|
|
492
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
493
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
494
|
+
driven.add(key);
|
|
495
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
496
|
+
if (ch.target === "deform") {
|
|
497
|
+
const att = attachments.get(ch.id);
|
|
498
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
499
|
+
else {
|
|
500
|
+
const n = att.weights ? att.weights.reduce((s, a) => s + a.length * 2, 0) : att.vertices.length;
|
|
501
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
505
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
506
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
445
510
|
for (const a of doc.animations) {
|
|
446
511
|
const tracks = /* @__PURE__ */ new Set();
|
|
447
512
|
for (const t of a.tracks) {
|
|
448
513
|
const tid = trackId(t);
|
|
449
514
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
450
515
|
tracks.add(tid);
|
|
451
|
-
|
|
516
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
517
|
+
const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : t.target === "control" ? controls.has(t.id) : true;
|
|
452
518
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
453
519
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
454
520
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
455
521
|
let prev = -1;
|
|
456
522
|
for (const k of t.keys) {
|
|
523
|
+
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`);
|
|
457
524
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
458
525
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
526
|
+
if (t.target === "control") {
|
|
527
|
+
const c = controls.get(t.id);
|
|
528
|
+
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`);
|
|
529
|
+
}
|
|
459
530
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
460
531
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
461
532
|
else if (typeof k.v === "string") {
|
|
@@ -496,7 +567,7 @@ function parse(input) {
|
|
|
496
567
|
|
|
497
568
|
// src/rig-api.ts
|
|
498
569
|
import { z as z3 } from "zod";
|
|
499
|
-
import { readFile as readFile9, writeFile as writeFile7, stat as stat2 } from "node:fs/promises";
|
|
570
|
+
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
500
571
|
import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
|
|
501
572
|
|
|
502
573
|
// src/rig-io.ts
|
|
@@ -544,6 +615,65 @@ async function withRigQueue(path, op) {
|
|
|
544
615
|
}
|
|
545
616
|
}
|
|
546
617
|
|
|
618
|
+
// src/rig-controls.ts
|
|
619
|
+
function controlPose(doc, values2) {
|
|
620
|
+
const out = /* @__PURE__ */ new Map();
|
|
621
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
622
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
623
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
624
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
625
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function bracket(keys, value) {
|
|
630
|
+
let i = 0;
|
|
631
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
632
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
633
|
+
}
|
|
634
|
+
function sampleBinding(binding, values2) {
|
|
635
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
636
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
637
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
638
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
639
|
+
return binding.channels.map((ch) => {
|
|
640
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
641
|
+
const at = (j2) => {
|
|
642
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
643
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
644
|
+
};
|
|
645
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function resolveControls(doc, input) {
|
|
649
|
+
if (!doc.controlBindings?.length) return input;
|
|
650
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
651
|
+
for (const b of doc.controlBindings) {
|
|
652
|
+
const values2 = b.axes.map((axis) => {
|
|
653
|
+
const c = controls.get(axis.control);
|
|
654
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
655
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
656
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
657
|
+
});
|
|
658
|
+
const sampled2 = sampleBinding(b, values2);
|
|
659
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
function controlCases(doc, probes) {
|
|
664
|
+
let cases = [{ label: "default", values: {} }];
|
|
665
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
666
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
667
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
668
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
669
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
670
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
671
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
672
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
673
|
+
}
|
|
674
|
+
return cases;
|
|
675
|
+
}
|
|
676
|
+
|
|
547
677
|
// src/rig-anim.ts
|
|
548
678
|
var DEG2 = Math.PI / 180;
|
|
549
679
|
var named = {
|
|
@@ -618,8 +748,8 @@ function sample(track, t, loop = false) {
|
|
|
618
748
|
}
|
|
619
749
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
620
750
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
621
|
-
const
|
|
622
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
751
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
752
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
623
753
|
}
|
|
624
754
|
return a.v;
|
|
625
755
|
}
|
|
@@ -647,6 +777,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
647
777
|
}
|
|
648
778
|
function restValue(doc, key) {
|
|
649
779
|
const [target, id, prop] = key.split(":");
|
|
780
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
650
781
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
651
782
|
if (target === "slot") {
|
|
652
783
|
const s = doc.slots.find((s2) => s2.id === id);
|
|
@@ -710,6 +841,13 @@ function boneWorldOf(bone, s, parent) {
|
|
|
710
841
|
if (!parent) return local;
|
|
711
842
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
712
843
|
const [tx, ty] = apply(parent, s.x, s.y);
|
|
844
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
845
|
+
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);
|
|
846
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
847
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
848
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
849
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
|
|
850
|
+
}
|
|
713
851
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
714
852
|
}
|
|
715
853
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -742,18 +880,20 @@ var localOf = (doc, pose, b) => {
|
|
|
742
880
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
743
881
|
};
|
|
744
882
|
};
|
|
745
|
-
function updateSubtree(skel, root2) {
|
|
883
|
+
function updateSubtree(skel, root2, includeRoot = true) {
|
|
746
884
|
const doc = skel.doc;
|
|
747
885
|
const kids = /* @__PURE__ */ new Map();
|
|
748
886
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
749
|
-
const stack = [root2];
|
|
887
|
+
const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
|
|
750
888
|
while (stack.length) {
|
|
751
889
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
752
890
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
753
891
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
754
892
|
}
|
|
755
893
|
}
|
|
756
|
-
function evaluate(doc, pose) {
|
|
894
|
+
function evaluate(doc, pose, modifiers) {
|
|
895
|
+
pose = resolveControls(doc, pose);
|
|
896
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
757
897
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
758
898
|
for (const b of boneOrder(doc)) {
|
|
759
899
|
const s = localOf(doc, pose, b);
|
|
@@ -987,7 +1127,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
987
1127
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
988
1128
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
989
1129
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
990
|
-
const
|
|
1130
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1131
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
991
1132
|
for (const id of c.bones) {
|
|
992
1133
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
993
1134
|
if (c.local) {
|
|
@@ -1149,6 +1290,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1149
1290
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1150
1291
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1151
1292
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1293
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1152
1294
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1153
1295
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1154
1296
|
let sum = 0;
|
|
@@ -1183,9 +1325,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1183
1325
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1184
1326
|
}
|
|
1185
1327
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1328
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1186
1329
|
for (let i = 0; i < bones.length; i++) {
|
|
1187
1330
|
const { b, st } = bones[i];
|
|
1188
|
-
let w = { ...
|
|
1331
|
+
let w = { ...inputWorlds[i] };
|
|
1189
1332
|
w.tx += (boneX - w.tx) * mixX;
|
|
1190
1333
|
w.ty += (boneY - w.ty) * mixY;
|
|
1191
1334
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1215,7 +1358,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1215
1358
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1216
1359
|
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 };
|
|
1217
1360
|
}
|
|
1218
|
-
|
|
1361
|
+
solvedWorlds.set(b.id, w);
|
|
1362
|
+
}
|
|
1363
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1364
|
+
const world = solvedWorlds.get(b.id);
|
|
1365
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1219
1366
|
}
|
|
1220
1367
|
}
|
|
1221
1368
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1244,9 +1391,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1244
1391
|
case "onlyTranslation":
|
|
1245
1392
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1246
1393
|
case "noRotationOrReflection": {
|
|
1247
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1394
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1248
1395
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1249
|
-
|
|
1396
|
+
if (lenX * lenX <= 1e-4) {
|
|
1397
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1398
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1399
|
+
}
|
|
1400
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1401
|
+
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 };
|
|
1250
1402
|
}
|
|
1251
1403
|
case "noScale":
|
|
1252
1404
|
case "noScaleOrReflection": {
|
|
@@ -1272,7 +1424,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1272
1424
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1273
1425
|
st.shearX = 0;
|
|
1274
1426
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1275
|
-
|
|
1427
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1428
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1429
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1430
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1431
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1432
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1433
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1434
|
+
}
|
|
1435
|
+
st.world = world;
|
|
1436
|
+
updateSubtree(skel, bone.id, false);
|
|
1276
1437
|
}
|
|
1277
1438
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1278
1439
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1979,6 +2140,9 @@ function updateBone(doc, id, patch) {
|
|
|
1979
2140
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1980
2141
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1981
2142
|
const cur = doc.bones[i];
|
|
2143
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2144
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2145
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1982
2146
|
const { parent, ...rest } = patch;
|
|
1983
2147
|
const next = RigSchema.shape.bones.element.parse({
|
|
1984
2148
|
...cur,
|
|
@@ -2011,6 +2175,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2011
2175
|
return { ...doc, animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.map((t) => t.target !== "drawOrder" ? t : { ...t, keys: t.keys.map((k) => ({ ...k, v: fn(k.v) })) }) })) };
|
|
2012
2176
|
}
|
|
2013
2177
|
function setSlot(doc, slot) {
|
|
2178
|
+
if (slot.alpha !== void 0) {
|
|
2179
|
+
for (const b of doc.controlBindings ?? [])
|
|
2180
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2181
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2182
|
+
}
|
|
2014
2183
|
const cur = doc.slots.find((s) => s.id === slot.id);
|
|
2015
2184
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2016
2185
|
const base = doc.slots.find((s) => s.id === slot.id);
|
|
@@ -2023,7 +2192,7 @@ function removeSlot(doc, id) {
|
|
|
2023
2192
|
return patchDrawOrder({
|
|
2024
2193
|
...doc,
|
|
2025
2194
|
slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
|
|
2026
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2195
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2027
2196
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2028
2197
|
}, (o) => o.filter((s) => s !== id));
|
|
2029
2198
|
}
|
|
@@ -2385,6 +2554,7 @@ function trackSummary(t) {
|
|
|
2385
2554
|
function rename2(doc, kind, id, to) {
|
|
2386
2555
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2387
2556
|
if (to === id) return doc;
|
|
2557
|
+
if (doc.controlBindings) doc = { ...doc, controlBindings: doc.controlBindings.map((b) => ({ ...b, channels: b.channels.map((ch) => ch.id === id && (kind === ch.target || kind === "attachment" && ch.target === "deform") ? { ...ch, id: to } : ch) })) };
|
|
2388
2558
|
const sub = (v) => v === id ? to : v;
|
|
2389
2559
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2390
2560
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2418,7 +2588,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2418
2588
|
return {
|
|
2419
2589
|
...doc,
|
|
2420
2590
|
slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
|
|
2421
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2591
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2422
2592
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2423
2593
|
};
|
|
2424
2594
|
}
|
|
@@ -2466,6 +2636,8 @@ function describeAttachment(doc, att) {
|
|
|
2466
2636
|
}
|
|
2467
2637
|
function describe(doc, opts = {}) {
|
|
2468
2638
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2639
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2640
|
+
for (const b of doc.controlBindings ?? []) lines.push(`keyforms ${b.id}: ${b.axes.map((a) => `${a.control}[${a.keys.join(",")}]`).join(" \xD7 ")} \u2192 ${b.channels.map((c) => `${c.target}.${c.id}.${c.prop}`).join(", ")}`);
|
|
2469
2641
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2470
2642
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2471
2643
|
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) {
|
|
@@ -5269,14 +5598,88 @@ function previewCheck(doc, animation) {
|
|
|
5269
5598
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5270
5599
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5271
5600
|
}
|
|
5272
|
-
var TrackRef = { interpolation: z3.enum(["segment", "pchip"]).optional().describe("pchip: smooth through numeric keys, preserving extrema; segment: per-key easing"), target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder"]), id: z3.string(), prop: z3.string() };
|
|
5601
|
+
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() };
|
|
5602
|
+
tool(
|
|
5603
|
+
"rig_set_control",
|
|
5604
|
+
"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.",
|
|
5605
|
+
{ path: Path, control: RigControl },
|
|
5606
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5607
|
+
);
|
|
5608
|
+
tool(
|
|
5609
|
+
"rig_set_control_binding",
|
|
5610
|
+
"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.",
|
|
5611
|
+
{ path: Path, binding: ControlBinding },
|
|
5612
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5613
|
+
);
|
|
5614
|
+
tool(
|
|
5615
|
+
"rig_remove_control_binding",
|
|
5616
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5617
|
+
{ path: Path, id: z3.string() },
|
|
5618
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5619
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5620
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5621
|
+
})
|
|
5622
|
+
);
|
|
5623
|
+
tool(
|
|
5624
|
+
"rig_remove_control",
|
|
5625
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5626
|
+
{ path: Path, id: z3.string() },
|
|
5627
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5628
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5629
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5630
|
+
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) })) };
|
|
5631
|
+
})
|
|
5632
|
+
);
|
|
5633
|
+
tool(
|
|
5634
|
+
"rig_sweep_controls",
|
|
5635
|
+
"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.",
|
|
5636
|
+
{
|
|
5637
|
+
path: Path,
|
|
5638
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5639
|
+
output: z3.string(),
|
|
5640
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5641
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5642
|
+
},
|
|
5643
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5644
|
+
try {
|
|
5645
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5646
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5647
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5648
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5649
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5650
|
+
await mkdir6(out, { recursive: true });
|
|
5651
|
+
const frames = [];
|
|
5652
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5653
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5654
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5655
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5656
|
+
}
|
|
5657
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5658
|
+
const report = {
|
|
5659
|
+
version: 1,
|
|
5660
|
+
sourceRig: sha256(raw),
|
|
5661
|
+
identity: r.identity,
|
|
5662
|
+
scale,
|
|
5663
|
+
region: region ?? null,
|
|
5664
|
+
...qa,
|
|
5665
|
+
frames,
|
|
5666
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5667
|
+
};
|
|
5668
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5669
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5670
|
+
} catch (e) {
|
|
5671
|
+
return fail(e);
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
);
|
|
5273
5675
|
var KeyIn = z3.object({
|
|
5274
5676
|
t: z3.number().describe("seconds"),
|
|
5275
5677
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5678
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5276
5679
|
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")
|
|
5277
5680
|
});
|
|
5278
5681
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5279
|
-
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].";
|
|
5682
|
+
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].";
|
|
5280
5683
|
tool(
|
|
5281
5684
|
"rig_create",
|
|
5282
5685
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5400,19 +5803,20 @@ tool(
|
|
|
5400
5803
|
);
|
|
5401
5804
|
tool(
|
|
5402
5805
|
"rig_import_spine",
|
|
5403
|
-
"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;
|
|
5806
|
+
"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.",
|
|
5404
5807
|
{
|
|
5405
5808
|
path: Path,
|
|
5406
5809
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5407
5810
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5408
5811
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5812
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5409
5813
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5410
5814
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5411
5815
|
notes: z3.string().optional()
|
|
5412
5816
|
},
|
|
5413
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5817
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5414
5818
|
try {
|
|
5415
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5819
|
+
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 });
|
|
5416
5820
|
const res = await create(path, parse(r.doc));
|
|
5417
5821
|
if (r.warnings.length) res.text += `
|
|
5418
5822
|
warnings:
|
|
@@ -6101,8 +6505,8 @@ tool(
|
|
|
6101
6505
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6102
6506
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6103
6507
|
if (framesDir) {
|
|
6104
|
-
const { mkdir:
|
|
6105
|
-
await
|
|
6508
|
+
const { mkdir: mkdir8 } = await import("node:fs/promises");
|
|
6509
|
+
await mkdir8(framesDir, { recursive: true });
|
|
6106
6510
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6107
6511
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6108
6512
|
const manifest = {
|
|
@@ -6252,7 +6656,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6252
6656
|
async function listRigs() {
|
|
6253
6657
|
const out = [];
|
|
6254
6658
|
const walk = async (dir) => {
|
|
6255
|
-
for (const e of await
|
|
6659
|
+
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
6256
6660
|
if (e.isDirectory()) {
|
|
6257
6661
|
if (!SKIP.has(e.name)) await walk(join10(dir, e.name));
|
|
6258
6662
|
continue;
|
|
@@ -6272,7 +6676,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6272
6676
|
}
|
|
6273
6677
|
async function listFiles(rigAbs) {
|
|
6274
6678
|
const dir = dirname9(rigAbs), out = [];
|
|
6275
|
-
for (const e of (await
|
|
6679
|
+
for (const e of (await readdir5(dir, { withFileTypes: true })).filter((e2) => e2.isFile() && /\.png$/i.test(e2.name)).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6276
6680
|
const fh = await open(join10(dir, e.name));
|
|
6277
6681
|
try {
|
|
6278
6682
|
const head = Buffer.alloc(24);
|
|
@@ -6390,7 +6794,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6390
6794
|
const abs = inside(rig);
|
|
6391
6795
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6392
6796
|
if (await stat3(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
|
|
6393
|
-
await
|
|
6797
|
+
await mkdir7(dirname9(abs), { recursive: true });
|
|
6394
6798
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6395
6799
|
ensureWatch(abs);
|
|
6396
6800
|
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
|