@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/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve as resolve3 } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
// src/editor/server.ts
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { readFile as readFile10, stat as stat3, readdir as
|
|
6
|
+
import { readFile as readFile10, stat as stat3, readdir as readdir5, mkdir as mkdir7, open } from "node:fs/promises";
|
|
7
7
|
import { watch as fsWatch } from "node:fs";
|
|
8
8
|
import { resolve as resolve2, dirname as dirname9, basename as basename5, extname as extname2, sep, join as join10, relative as relative3 } from "node:path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -176,7 +176,9 @@ var PathAttachment = z.object({
|
|
|
176
176
|
/** curve lengths for constant-speed positioning */
|
|
177
177
|
lengths: z.array(z.number()).default([]),
|
|
178
178
|
closed: z.boolean().default(false),
|
|
179
|
-
constantSpeed: z.boolean().default(true)
|
|
179
|
+
constantSpeed: z.boolean().default(true),
|
|
180
|
+
/** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
|
|
181
|
+
clipping: z.object({ end: Id.optional() }).optional()
|
|
180
182
|
});
|
|
181
183
|
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
182
184
|
var IkConstraint = z.object({
|
|
@@ -245,10 +247,13 @@ var Key = z.object({
|
|
|
245
247
|
t: z.number().nonnegative(),
|
|
246
248
|
v: KeyValue,
|
|
247
249
|
/** easing INTO the next key */
|
|
248
|
-
ease: Ease.default("ease_in_out")
|
|
250
|
+
ease: Ease.default("ease_in_out"),
|
|
251
|
+
/** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
|
|
252
|
+
componentEase: z.array(Ease).min(1).optional()
|
|
249
253
|
});
|
|
250
254
|
var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
|
|
251
255
|
var Track = z.discriminatedUnion("target", [
|
|
256
|
+
z.object({ target: z.literal("control"), id: Id, prop: z.literal("value"), ...Interpolation, keys: z.array(Key).min(1) }),
|
|
252
257
|
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
253
258
|
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
254
259
|
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
|
|
@@ -271,6 +276,25 @@ var Animation = z.object({
|
|
|
271
276
|
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
272
277
|
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
273
278
|
});
|
|
279
|
+
var RigControl = z.object({
|
|
280
|
+
id: Id,
|
|
281
|
+
label: z.string().optional(),
|
|
282
|
+
min: z.number().finite(),
|
|
283
|
+
max: z.number().finite(),
|
|
284
|
+
default: z.number().finite()
|
|
285
|
+
}).strict();
|
|
286
|
+
var ControlValues = z.array(z.union([z.number().finite(), z.array(z.number().finite()).min(1)])).min(2);
|
|
287
|
+
var ControlChannel = z.discriminatedUnion("target", [
|
|
288
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
|
|
289
|
+
z.object({ target: z.literal("slot"), id: Id, prop: z.literal("alpha"), values: ControlValues }).strict(),
|
|
290
|
+
z.object({ target: z.literal("deform"), id: Id, prop: z.literal("vertices"), values: ControlValues }).strict()
|
|
291
|
+
]);
|
|
292
|
+
var ControlBinding = z.object({
|
|
293
|
+
id: Id,
|
|
294
|
+
axes: z.array(z.object({ control: Id, keys: z.array(z.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
|
|
295
|
+
/** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
|
|
296
|
+
channels: z.array(ControlChannel).min(1)
|
|
297
|
+
}).strict();
|
|
274
298
|
var Reference = z.object({
|
|
275
299
|
src: z.string(),
|
|
276
300
|
opacity: z.number().min(0).max(1).default(0.5),
|
|
@@ -296,7 +320,9 @@ var RigSchema = z.object({
|
|
|
296
320
|
ik: z.array(IkConstraint).default([]),
|
|
297
321
|
transform: z.array(TransformConstraint).default([]),
|
|
298
322
|
path: z.array(PathConstraint).default([]),
|
|
299
|
-
animations: z.array(Animation)
|
|
323
|
+
animations: z.array(Animation),
|
|
324
|
+
controls: z.array(RigControl).optional(),
|
|
325
|
+
controlBindings: z.array(ControlBinding).optional()
|
|
300
326
|
});
|
|
301
327
|
var RigDocument = z.preprocess((input) => {
|
|
302
328
|
const v = input?.version;
|
|
@@ -371,6 +397,8 @@ function validate(doc) {
|
|
|
371
397
|
dup(doc.ik, "ik");
|
|
372
398
|
dup(doc.transform, "transform");
|
|
373
399
|
dup(doc.path, "path");
|
|
400
|
+
dup(doc.controls ?? [], "control");
|
|
401
|
+
dup(doc.controlBindings ?? [], "control binding");
|
|
374
402
|
const bones = new Set(doc.bones.map((b) => b.id));
|
|
375
403
|
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
376
404
|
const slots = new Set(doc.slots.map((s2) => s2.id));
|
|
@@ -408,6 +436,10 @@ function validate(doc) {
|
|
|
408
436
|
else if (a.slot !== s2.id) errs.push(`slot "${s2.id}": attachment "${s2.attachment}" belongs to slot "${a.slot}"`);
|
|
409
437
|
}
|
|
410
438
|
}
|
|
439
|
+
for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
|
|
440
|
+
if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
|
|
441
|
+
if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
|
|
442
|
+
}
|
|
411
443
|
const vertexCount2 = (a) => {
|
|
412
444
|
if (a.type === "region") return 4;
|
|
413
445
|
if (a.type === "mesh") return a.uvs.length / 2;
|
|
@@ -445,20 +477,59 @@ function validate(doc) {
|
|
|
445
477
|
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
446
478
|
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
447
479
|
}
|
|
480
|
+
const controls = new Map((doc.controls ?? []).map((c) => [c.id, c]));
|
|
481
|
+
const driven = /* @__PURE__ */ new Set();
|
|
482
|
+
for (const c of controls.values()) if (!(c.min < c.max && c.default >= c.min && c.default <= c.max))
|
|
483
|
+
errs.push(`control "${c.id}": require min < max and default within range`);
|
|
484
|
+
for (const b of doc.controlBindings ?? []) {
|
|
485
|
+
const prefix = `control binding "${b.id}"`;
|
|
486
|
+
if (new Set(b.axes.map((a) => a.control)).size !== b.axes.length) errs.push(`${prefix}: axes must use different controls`);
|
|
487
|
+
for (const axis of b.axes) {
|
|
488
|
+
const c = controls.get(axis.control);
|
|
489
|
+
if (!c) errs.push(`${prefix}: unknown control "${axis.control}"`);
|
|
490
|
+
else if (axis.keys[0] !== c.min || axis.keys.at(-1) !== c.max) errs.push(`${prefix}: keys must span the control's full range`);
|
|
491
|
+
if (axis.keys.some((v, i) => i > 0 && v <= axis.keys[i - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
|
|
492
|
+
}
|
|
493
|
+
const count = b.axes.reduce((n, a) => n * a.keys.length, 1);
|
|
494
|
+
for (const ch of b.channels) {
|
|
495
|
+
const key = `${ch.target}.${ch.id}.${ch.prop}`;
|
|
496
|
+
if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
|
|
497
|
+
driven.add(key);
|
|
498
|
+
if (ch.values.length !== count) errs.push(`${prefix}: ${key} needs ${count} grid values`);
|
|
499
|
+
if (ch.target === "deform") {
|
|
500
|
+
const att = attachments.get(ch.id);
|
|
501
|
+
if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
|
|
502
|
+
else {
|
|
503
|
+
const n = att.weights ? att.weights.reduce((s2, a) => s2 + a.length * 2, 0) : att.vertices.length;
|
|
504
|
+
if (ch.values.some((v) => !Array.isArray(v) || v.length !== n)) errs.push(`${prefix}: ${key} needs arrays of ${n} offsets`);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
|
|
508
|
+
if (ch.values.some((v) => typeof v !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
|
|
509
|
+
if (ch.target === "slot" && ch.values.some((v) => typeof v === "number" && (v < 0 || v > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
448
513
|
for (const a of doc.animations) {
|
|
449
514
|
const tracks = /* @__PURE__ */ new Set();
|
|
450
515
|
for (const t of a.tracks) {
|
|
451
516
|
const tid = trackId(t);
|
|
452
517
|
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
453
518
|
tracks.add(tid);
|
|
454
|
-
|
|
519
|
+
if (driven.has(tid)) errs.push(`animation "${a.id}": ${tid} is controlled by a binding; animate its control instead`);
|
|
520
|
+
const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : t.target === "control" ? controls.has(t.id) : true;
|
|
455
521
|
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
456
522
|
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
457
523
|
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
458
524
|
let prev = -1;
|
|
459
525
|
for (const k of t.keys) {
|
|
526
|
+
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`);
|
|
460
527
|
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
461
528
|
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
529
|
+
if (t.target === "control") {
|
|
530
|
+
const c = controls.get(t.id);
|
|
531
|
+
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`);
|
|
532
|
+
}
|
|
462
533
|
if (t.target === "slot" && t.prop === "attachment") {
|
|
463
534
|
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
464
535
|
else if (typeof k.v === "string") {
|
|
@@ -499,7 +570,7 @@ function parse(input) {
|
|
|
499
570
|
|
|
500
571
|
// src/rig-api.ts
|
|
501
572
|
import { z as z3 } from "zod";
|
|
502
|
-
import { readFile as readFile9, writeFile as writeFile7, stat as stat2 } from "node:fs/promises";
|
|
573
|
+
import { readFile as readFile9, writeFile as writeFile7, stat as stat2, mkdir as mkdir6, readdir as readdir4 } from "node:fs/promises";
|
|
503
574
|
import { dirname as dirname8, join as join9, relative as relative2, isAbsolute } from "node:path";
|
|
504
575
|
|
|
505
576
|
// src/rig-io.ts
|
|
@@ -547,6 +618,65 @@ async function withRigQueue(path, op) {
|
|
|
547
618
|
}
|
|
548
619
|
}
|
|
549
620
|
|
|
621
|
+
// src/rig-controls.ts
|
|
622
|
+
function controlPose(doc, values2) {
|
|
623
|
+
const out = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const [id, value] of Object.entries(values2)) {
|
|
625
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
626
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
627
|
+
if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
|
|
628
|
+
out.set(`control:${id}:value`, Math.max(c.min, Math.min(c.max, value)));
|
|
629
|
+
}
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
function bracket(keys, value) {
|
|
633
|
+
let i = 0;
|
|
634
|
+
while (i < keys.length - 2 && value > keys[i + 1]) i++;
|
|
635
|
+
return [i, Math.max(0, Math.min(1, (value - keys[i]) / (keys[i + 1] - keys[i])))];
|
|
636
|
+
}
|
|
637
|
+
function sampleBinding(binding, values2) {
|
|
638
|
+
const [x, u] = bracket(binding.axes[0].keys, values2[0]);
|
|
639
|
+
const [y, v] = binding.axes[1] ? bracket(binding.axes[1].keys, values2[1]) : [0, 0];
|
|
640
|
+
const width = binding.axes[0].keys.length, a = y * width + x;
|
|
641
|
+
const mix = (a2, b, u2) => a2 + (b - a2) * u2;
|
|
642
|
+
return binding.channels.map((ch) => {
|
|
643
|
+
const read = (i, j2) => j2 === void 0 ? ch.values[i] : ch.values[i][j2];
|
|
644
|
+
const at = (j2) => {
|
|
645
|
+
const top = mix(read(a, j2), read(a + 1, j2), u);
|
|
646
|
+
return binding.axes[1] ? mix(top, mix(read(a + width, j2), read(a + width + 1, j2), u), v) : top;
|
|
647
|
+
};
|
|
648
|
+
return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function resolveControls(doc, input) {
|
|
652
|
+
if (!doc.controlBindings?.length) return input;
|
|
653
|
+
const out = new Map(input), controls = new Map(doc.controls?.map((c) => [c.id, c]));
|
|
654
|
+
for (const b of doc.controlBindings) {
|
|
655
|
+
const values2 = b.axes.map((axis) => {
|
|
656
|
+
const c = controls.get(axis.control);
|
|
657
|
+
const value = input.get(`control:${c.id}:value`) ?? c.default;
|
|
658
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c.id}" must be finite`);
|
|
659
|
+
return Math.max(c.min, Math.min(c.max, value));
|
|
660
|
+
});
|
|
661
|
+
const sampled2 = sampleBinding(b, values2);
|
|
662
|
+
b.channels.forEach((ch, i) => out.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled2[i]));
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function controlCases(doc, probes) {
|
|
667
|
+
let cases = [{ label: "default", values: {} }];
|
|
668
|
+
if (!Object.keys(probes).length) throw new Error("select controls and sample values for the sweep");
|
|
669
|
+
for (const [id, values2] of Object.entries(probes)) {
|
|
670
|
+
const c = doc.controls?.find((c2) => c2.id === id);
|
|
671
|
+
if (!c) throw new Error(`unknown control "${id}"`);
|
|
672
|
+
if (!values2.length || values2.some((v) => !Number.isFinite(v) || v < c.min || v > c.max) || new Set(values2).size !== values2.length)
|
|
673
|
+
throw new Error(`control "${id}": probes must be unique finite values inside its range`);
|
|
674
|
+
if (cases.length * values2.length > 256) throw new Error("control sweep exceeds 256 poses; choose a smaller grid");
|
|
675
|
+
cases = cases.flatMap((p) => values2.map((v) => ({ label: p.label === "default" ? `${id}=${v}` : `${p.label}, ${id}=${v}`, values: { ...p.values, [id]: v } })));
|
|
676
|
+
}
|
|
677
|
+
return cases;
|
|
678
|
+
}
|
|
679
|
+
|
|
550
680
|
// src/rig-anim.ts
|
|
551
681
|
var DEG2 = Math.PI / 180;
|
|
552
682
|
var named = {
|
|
@@ -621,8 +751,8 @@ function sample(track, t, loop = false) {
|
|
|
621
751
|
}
|
|
622
752
|
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
623
753
|
if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
|
|
624
|
-
const
|
|
625
|
-
return av.map((v, k) => v + ((bv[k] ?? v) - v) *
|
|
754
|
+
const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
|
|
755
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
|
|
626
756
|
}
|
|
627
757
|
return a.v;
|
|
628
758
|
}
|
|
@@ -650,6 +780,7 @@ function blendPoses(from, to, w, restOf) {
|
|
|
650
780
|
}
|
|
651
781
|
function restValue(doc, key) {
|
|
652
782
|
const [target, id, prop] = key.split(":");
|
|
783
|
+
if (target === "control") return doc.controls?.find((c) => c.id === id)?.default ?? 0;
|
|
653
784
|
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
654
785
|
if (target === "slot") {
|
|
655
786
|
const s2 = doc.slots.find((s3) => s3.id === id);
|
|
@@ -713,6 +844,13 @@ function boneWorldOf(bone, s2, parent) {
|
|
|
713
844
|
if (!parent) return local;
|
|
714
845
|
if (bone.inherit === "normal") return mul(parent, local);
|
|
715
846
|
const [tx, ty] = apply(parent, s2.x, s2.y);
|
|
847
|
+
if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
|
|
848
|
+
const r = s2.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
|
|
849
|
+
const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
|
|
850
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
851
|
+
const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
|
|
852
|
+
return { ...mul(basis3, affineOf(0, 0, 0, s2.scaleX, s2.scaleY, s2.shearX, s2.shearY)), tx, ty };
|
|
853
|
+
}
|
|
716
854
|
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
717
855
|
}
|
|
718
856
|
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -745,18 +883,20 @@ var localOf = (doc, pose, b) => {
|
|
|
745
883
|
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
746
884
|
};
|
|
747
885
|
};
|
|
748
|
-
function updateSubtree(skel, root3) {
|
|
886
|
+
function updateSubtree(skel, root3, includeRoot = true) {
|
|
749
887
|
const doc = skel.doc;
|
|
750
888
|
const kids = /* @__PURE__ */ new Map();
|
|
751
889
|
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
752
|
-
const stack = [root3];
|
|
890
|
+
const stack = includeRoot ? [root3] : (kids.get(root3) ?? []).map((b) => b.id);
|
|
753
891
|
while (stack.length) {
|
|
754
892
|
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
755
893
|
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
756
894
|
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
757
895
|
}
|
|
758
896
|
}
|
|
759
|
-
function evaluate(doc, pose) {
|
|
897
|
+
function evaluate(doc, pose, modifiers) {
|
|
898
|
+
pose = resolveControls(doc, pose);
|
|
899
|
+
if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
|
|
760
900
|
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
761
901
|
for (const b of boneOrder(doc)) {
|
|
762
902
|
const s2 = localOf(doc, pose, b);
|
|
@@ -990,7 +1130,8 @@ function applyTransformConstraint(skel, c) {
|
|
|
990
1130
|
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
991
1131
|
c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
|
|
992
1132
|
const target = skel.bones.get(c.target), tw = target.world;
|
|
993
|
-
const
|
|
1133
|
+
const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
|
|
1134
|
+
const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
|
|
994
1135
|
for (const id of c.bones) {
|
|
995
1136
|
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
996
1137
|
if (c.local) {
|
|
@@ -1152,6 +1293,7 @@ function applyPathConstraint(skel, c) {
|
|
|
1152
1293
|
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1153
1294
|
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1154
1295
|
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1296
|
+
const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
|
|
1155
1297
|
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1156
1298
|
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1157
1299
|
let sum = 0;
|
|
@@ -1186,9 +1328,10 @@ function applyPathConstraint(skel, c) {
|
|
|
1186
1328
|
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1187
1329
|
}
|
|
1188
1330
|
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1331
|
+
const solvedWorlds = /* @__PURE__ */ new Map();
|
|
1189
1332
|
for (let i = 0; i < bones.length; i++) {
|
|
1190
1333
|
const { b, st } = bones[i];
|
|
1191
|
-
let w = { ...
|
|
1334
|
+
let w = { ...inputWorlds[i] };
|
|
1192
1335
|
w.tx += (boneX - w.tx) * mixX;
|
|
1193
1336
|
w.ty += (boneY - w.ty) * mixY;
|
|
1194
1337
|
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
@@ -1218,7 +1361,11 @@ function applyPathConstraint(skel, c) {
|
|
|
1218
1361
|
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1219
1362
|
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 };
|
|
1220
1363
|
}
|
|
1221
|
-
|
|
1364
|
+
solvedWorlds.set(b.id, w);
|
|
1365
|
+
}
|
|
1366
|
+
for (const b of boneOrder(skel.doc)) {
|
|
1367
|
+
const world = solvedWorlds.get(b.id);
|
|
1368
|
+
if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
|
|
1222
1369
|
}
|
|
1223
1370
|
}
|
|
1224
1371
|
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
@@ -1247,9 +1394,14 @@ function inheritedParentLinear(bone, parent) {
|
|
|
1247
1394
|
case "onlyTranslation":
|
|
1248
1395
|
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1249
1396
|
case "noRotationOrReflection": {
|
|
1250
|
-
const lenX = Math.hypot(parent.a, parent.b)
|
|
1397
|
+
const lenX = Math.hypot(parent.a, parent.b);
|
|
1251
1398
|
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1252
|
-
|
|
1399
|
+
if (lenX * lenX <= 1e-4) {
|
|
1400
|
+
const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
|
|
1401
|
+
return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
|
|
1402
|
+
}
|
|
1403
|
+
const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
|
|
1404
|
+
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 };
|
|
1253
1405
|
}
|
|
1254
1406
|
case "noScale":
|
|
1255
1407
|
case "noScaleOrReflection": {
|
|
@@ -1275,7 +1427,16 @@ function setEffectiveWorld(skel, bone, st, world) {
|
|
|
1275
1427
|
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1276
1428
|
st.shearX = 0;
|
|
1277
1429
|
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1278
|
-
|
|
1430
|
+
if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
|
|
1431
|
+
const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
|
|
1432
|
+
st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
|
|
1433
|
+
st.scaleX = Math.hypot(world.a, world.b);
|
|
1434
|
+
st.scaleY = Math.hypot(world.c, world.d);
|
|
1435
|
+
const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
|
|
1436
|
+
st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
|
|
1437
|
+
}
|
|
1438
|
+
st.world = world;
|
|
1439
|
+
updateSubtree(skel, bone.id, false);
|
|
1279
1440
|
}
|
|
1280
1441
|
function attachmentWorldVertices(skel, slotId, att) {
|
|
1281
1442
|
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
@@ -1982,6 +2143,9 @@ function updateBone(doc, id, patch) {
|
|
|
1982
2143
|
const i = doc.bones.findIndex((b) => b.id === id);
|
|
1983
2144
|
if (i < 0) throw new Error(`unknown bone "${id}"`);
|
|
1984
2145
|
const cur = doc.bones[i];
|
|
2146
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels)
|
|
2147
|
+
if (ch.target === "bone" && ch.id === id && patch.transform?.[ch.prop] !== void 0)
|
|
2148
|
+
throw new Error(`bone "${id}" ${ch.prop} is controlled by "${b.id}"; edit its keyforms`);
|
|
1985
2149
|
const { parent, ...rest } = patch;
|
|
1986
2150
|
const next = RigSchema.shape.bones.element.parse({
|
|
1987
2151
|
...cur,
|
|
@@ -2014,6 +2178,11 @@ function patchDrawOrder(doc, fn) {
|
|
|
2014
2178
|
return { ...doc, animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.map((t) => t.target !== "drawOrder" ? t : { ...t, keys: t.keys.map((k) => ({ ...k, v: fn(k.v) })) }) })) };
|
|
2015
2179
|
}
|
|
2016
2180
|
function setSlot(doc, slot) {
|
|
2181
|
+
if (slot.alpha !== void 0) {
|
|
2182
|
+
for (const b of doc.controlBindings ?? [])
|
|
2183
|
+
if (b.channels.some((ch) => ch.target === "slot" && ch.id === slot.id && ch.prop === "alpha"))
|
|
2184
|
+
throw new Error(`slot "${slot.id}" alpha is controlled by "${b.id}"; edit its keyforms`);
|
|
2185
|
+
}
|
|
2017
2186
|
const cur = doc.slots.find((s2) => s2.id === slot.id);
|
|
2018
2187
|
if (cur && slot.bone && slot.bone !== cur.bone) doc = rebindSlot(doc, slot.id, slot.bone).doc;
|
|
2019
2188
|
const base = doc.slots.find((s2) => s2.id === slot.id);
|
|
@@ -2026,7 +2195,7 @@ function removeSlot(doc, id) {
|
|
|
2026
2195
|
return patchDrawOrder({
|
|
2027
2196
|
...doc,
|
|
2028
2197
|
slots: doc.slots.filter((s2) => s2.id !== id).map((s2) => gone.has(s2.clip ?? "") ? { ...s2, clip: void 0 } : s2),
|
|
2029
|
-
attachments: doc.attachments.filter((a) => a.slot !== id),
|
|
2198
|
+
attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
|
|
2030
2199
|
animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
|
|
2031
2200
|
}, (o) => o.filter((s2) => s2 !== id));
|
|
2032
2201
|
}
|
|
@@ -2388,6 +2557,7 @@ function trackSummary(t) {
|
|
|
2388
2557
|
function rename2(doc, kind, id, to) {
|
|
2389
2558
|
if (!ID_RE.test(to)) throw new Error(`"${to}" is not a snake_case id`);
|
|
2390
2559
|
if (to === id) return doc;
|
|
2560
|
+
if (doc.controlBindings) doc = { ...doc, controlBindings: doc.controlBindings.map((b) => ({ ...b, channels: b.channels.map((ch) => ch.id === id && (kind === ch.target || kind === "attachment" && ch.target === "deform") ? { ...ch, id: to } : ch) })) };
|
|
2391
2561
|
const sub = (v) => v === id ? to : v;
|
|
2392
2562
|
const subKeys = (t, pred, f) => pred ? { ...t, keys: t.keys.map((k) => ({ ...k, v: f(k.v) })) } : t;
|
|
2393
2563
|
const retrack = (target) => (t) => t.target === target && t.id === id ? { ...t, id: to } : t;
|
|
@@ -2421,7 +2591,7 @@ function rename2(doc, kind, id, to) {
|
|
|
2421
2591
|
return {
|
|
2422
2592
|
...doc,
|
|
2423
2593
|
slots: doc.slots.map((s2) => ({ ...s2, id: sub(s2.id) })),
|
|
2424
|
-
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
|
|
2594
|
+
attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
|
|
2425
2595
|
path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
|
|
2426
2596
|
};
|
|
2427
2597
|
}
|
|
@@ -2469,6 +2639,8 @@ function describeAttachment(doc, att) {
|
|
|
2469
2639
|
}
|
|
2470
2640
|
function describe(doc, opts = {}) {
|
|
2471
2641
|
const lines = [`${doc.meta.name} ${doc.meta.width}x${doc.meta.height}`];
|
|
2642
|
+
for (const c of doc.controls ?? []) lines.push(`control ${c.id}: [${c.min}..${c.max}], default ${c.default}${c.label ? ` (${c.label})` : ""}`);
|
|
2643
|
+
for (const b of doc.controlBindings ?? []) lines.push(`keyforms ${b.id}: ${b.axes.map((a) => `${a.control}[${a.keys.join(",")}]`).join(" \xD7 ")} \u2192 ${b.channels.map((c) => `${c.target}.${c.id}.${c.prop}`).join(", ")}`);
|
|
2472
2644
|
if (doc.meta.notes) lines.push(`notes: ${doc.meta.notes}`);
|
|
2473
2645
|
const kids = (id) => doc.bones.filter((b) => b.parent === id);
|
|
2474
2646
|
const place = (s2) => {
|
|
@@ -3168,8 +3340,8 @@ function motionQuality(doc, anim, opts = {}) {
|
|
|
3168
3340
|
if (!skel.attachment.get(front) || !skel.attachment.get(back)) continue;
|
|
3169
3341
|
const visible = (id) => {
|
|
3170
3342
|
const slot = doc.slots.find((s2) => s2.id === id);
|
|
3171
|
-
const color = pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3172
|
-
return Number(pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3343
|
+
const color = skel.pose.get(pk("slot", id, "color")) ?? colorToArray(slot.color);
|
|
3344
|
+
return Number(skel.pose.get(pk("slot", id, "alpha")) ?? slot.alpha) * color[3] > 0;
|
|
3173
3345
|
};
|
|
3174
3346
|
if (!visible(front) || !visible(back)) continue;
|
|
3175
3347
|
const order = pose.get(pk("drawOrder", "order", "order"));
|
|
@@ -3244,6 +3416,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3244
3416
|
});
|
|
3245
3417
|
const W = doc.meta.width, H = doc.meta.height;
|
|
3246
3418
|
const overflowOf = (pose, slotId, skel) => {
|
|
3419
|
+
pose = skel.pose;
|
|
3247
3420
|
const pts = slotOutline(doc, pose, slotId, 4, skel);
|
|
3248
3421
|
if (!pts) return void 0;
|
|
3249
3422
|
const slot = doc.slots.find((s2) => s2.id === slotId), alphaV = pose.get(`slot:${slotId}:alpha`);
|
|
@@ -3442,6 +3615,7 @@ function checkRig(doc, opts = {}) {
|
|
|
3442
3615
|
}
|
|
3443
3616
|
}
|
|
3444
3617
|
const vertexKeyed = new Set(a.tracks.filter((t) => t.target === "deform" && t.prop === "vertices").map((t) => t.id));
|
|
3618
|
+
for (const b of doc.controlBindings ?? []) for (const ch of b.channels) if (ch.target === "deform") vertexKeyed.add(ch.id);
|
|
3445
3619
|
const deformMeshes = meshes.filter((att) => vertexKeyed.has(att.id));
|
|
3446
3620
|
const bonesMeshes = meshes.filter((att) => !vertexKeyed.has(att.id));
|
|
3447
3621
|
const meshFolds = /* @__PURE__ */ new Map();
|
|
@@ -3778,11 +3952,13 @@ function diffDocs(a, b) {
|
|
|
3778
3952
|
["attachments", "attachment"],
|
|
3779
3953
|
["ik", "ik"],
|
|
3780
3954
|
["transform", "transform"],
|
|
3781
|
-
["path", "path"]
|
|
3955
|
+
["path", "path"],
|
|
3956
|
+
["controls", "control"],
|
|
3957
|
+
["controlBindings", "control binding"]
|
|
3782
3958
|
];
|
|
3783
3959
|
for (const [kind, name] of kinds) {
|
|
3784
|
-
const ma = new Map(a[kind].map((x) => [x.id, x]));
|
|
3785
|
-
const mb = new Map(b[kind].map((x) => [x.id, x]));
|
|
3960
|
+
const ma = new Map((a[kind] ?? []).map((x) => [x.id, x]));
|
|
3961
|
+
const mb = new Map((b[kind] ?? []).map((x) => [x.id, x]));
|
|
3786
3962
|
for (const [id, x] of mb) if (!ma.has(id)) out.push(`+ ${name} ${id} ${j(x)}`);
|
|
3787
3963
|
for (const [id, x] of ma) {
|
|
3788
3964
|
const y = mb.get(id);
|
|
@@ -4028,6 +4204,9 @@ var Ids = class {
|
|
|
4028
4204
|
has(name) {
|
|
4029
4205
|
return this.map.has(name);
|
|
4030
4206
|
}
|
|
4207
|
+
reserve(id) {
|
|
4208
|
+
this.used.add(id);
|
|
4209
|
+
}
|
|
4031
4210
|
};
|
|
4032
4211
|
var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
|
|
4033
4212
|
var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
@@ -4036,6 +4215,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4036
4215
|
const warnings = [];
|
|
4037
4216
|
const atlas = parseAtlas(atlasText);
|
|
4038
4217
|
const version = String(json.skeleton?.spine ?? "4.0");
|
|
4218
|
+
if (json.physics?.length) warnings.push(`${json.physics.length} Spine physics constraints are not imported; bake their motion before delivery`);
|
|
4039
4219
|
const major = parseFloat(version);
|
|
4040
4220
|
const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
|
|
4041
4221
|
const margin = opts.margin ?? 0;
|
|
@@ -4043,6 +4223,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4043
4223
|
const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
|
|
4044
4224
|
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_");
|
|
4045
4225
|
const ROOT = "origin";
|
|
4226
|
+
boneIds.reserve(ROOT);
|
|
4046
4227
|
const attachmentIdMap = /* @__PURE__ */ new Map();
|
|
4047
4228
|
const attachmentId = (slot, name) => {
|
|
4048
4229
|
const key = `${slot} ${name}`;
|
|
@@ -4067,13 +4248,17 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4067
4248
|
});
|
|
4068
4249
|
}
|
|
4069
4250
|
const skins = Array.isArray(json.skins) ? json.skins : Object.entries(json.skins ?? {}).map(([name, attachments2]) => ({ name, attachments: attachments2 }));
|
|
4251
|
+
const selectedSkin = opts.skin ? skins.find((s2) => s2.name === opts.skin) : skins.find((s2) => s2.name === "default") ?? skins[0];
|
|
4252
|
+
if (opts.skin && !selectedSkin) throw new Error(`Spine skin "${opts.skin}" not found; available: ${skins.map((s2) => s2.name).join(", ")}`);
|
|
4253
|
+
const lookupSkins = [selectedSkin, skins.find((s2) => s2.name === "default")].filter((s2, i, list) => s2 && list.indexOf(s2) === i);
|
|
4254
|
+
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)`);
|
|
4070
4255
|
const frameSuffix = (seq, index) => {
|
|
4071
|
-
const n = String((seq.start ??
|
|
4256
|
+
const n = String((seq.start ?? 1) + index);
|
|
4072
4257
|
return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
|
|
4073
4258
|
};
|
|
4074
4259
|
const sequences = /* @__PURE__ */ new Map();
|
|
4075
4260
|
const skinnedName = /* @__PURE__ */ new Map();
|
|
4076
|
-
for (const skin2 of
|
|
4261
|
+
for (const skin2 of lookupSkins) {
|
|
4077
4262
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4078
4263
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
|
|
4079
4264
|
for (const [name, a] of Object.entries(atts)) {
|
|
@@ -4082,7 +4267,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4082
4267
|
if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
|
|
4083
4268
|
}
|
|
4084
4269
|
}
|
|
4085
|
-
const attachmentRef = (slot, name) =>
|
|
4270
|
+
const attachmentRef = (slot, name) => {
|
|
4271
|
+
const resolved = skinnedName.get(`${slot} ${name}`);
|
|
4272
|
+
return resolved === void 0 ? null : attachmentId(slot, resolved);
|
|
4273
|
+
};
|
|
4086
4274
|
const spineSlots = json.slots ?? [];
|
|
4087
4275
|
const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
|
|
4088
4276
|
const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
|
|
@@ -4090,11 +4278,12 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4090
4278
|
id: slotIds.get(s2.name),
|
|
4091
4279
|
bone: boneIds.get(s2.bone),
|
|
4092
4280
|
z: i,
|
|
4093
|
-
...s2.attachment ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4281
|
+
...s2.attachment && attachmentRef(s2.name, s2.attachment) ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
|
|
4094
4282
|
blend: blendOf(s2.blend),
|
|
4095
4283
|
color: hex8(s2.color),
|
|
4096
4284
|
alpha: 1
|
|
4097
4285
|
}));
|
|
4286
|
+
if (spineSlots.some((s2) => s2.dark)) warnings.push("Spine setup dark two-color tint is not supported");
|
|
4098
4287
|
const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
|
|
4099
4288
|
const assets = /* @__PURE__ */ new Map();
|
|
4100
4289
|
const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
|
|
@@ -4120,6 +4309,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4120
4309
|
return id;
|
|
4121
4310
|
};
|
|
4122
4311
|
const attachments = [];
|
|
4312
|
+
const clipDeforms = /* @__PURE__ */ new Map();
|
|
4123
4313
|
const readVertices = (a, vertexCount2) => {
|
|
4124
4314
|
const v = a.vertices ?? [];
|
|
4125
4315
|
if (v.length === vertexCount2 * 2) {
|
|
@@ -4140,6 +4330,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4140
4330
|
}
|
|
4141
4331
|
return { weights };
|
|
4142
4332
|
};
|
|
4333
|
+
const meshGeometry = (skinName, slot, name, visiting = /* @__PURE__ */ new Set()) => {
|
|
4334
|
+
const key = JSON.stringify([skinName, slot, name]);
|
|
4335
|
+
if (visiting.has(key)) {
|
|
4336
|
+
warnings.push(`linked mesh cycle at "${skinName}/${slot}/${name}" skipped`);
|
|
4337
|
+
return void 0;
|
|
4338
|
+
}
|
|
4339
|
+
const raw = skins.find((s2) => s2.name === skinName)?.attachments?.[slot]?.[name];
|
|
4340
|
+
if (!raw) {
|
|
4341
|
+
warnings.push(`linked mesh parent "${skinName}/${slot}/${name}" not found`);
|
|
4342
|
+
return void 0;
|
|
4343
|
+
}
|
|
4344
|
+
if (!raw.parent) return raw;
|
|
4345
|
+
visiting.add(key);
|
|
4346
|
+
return meshGeometry(raw.skin ?? "default", slot, raw.parent, visiting);
|
|
4347
|
+
};
|
|
4348
|
+
const inheritedDeform = /* @__PURE__ */ new Map();
|
|
4143
4349
|
for (const skin2 of skins) {
|
|
4144
4350
|
const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
|
|
4145
4351
|
for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
|
|
@@ -4151,7 +4357,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4151
4357
|
for (const [name, a] of Object.entries(atts)) {
|
|
4152
4358
|
if (a.sequence) {
|
|
4153
4359
|
sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
|
|
4154
|
-
const base = a.path ?? name;
|
|
4360
|
+
const base = a.path ?? a.name ?? name;
|
|
4155
4361
|
for (let i = 0; i < (a.sequence.count ?? 0); i++) {
|
|
4156
4362
|
const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
|
|
4157
4363
|
if (!asset) continue;
|
|
@@ -4174,7 +4380,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4174
4380
|
}
|
|
4175
4381
|
const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
|
|
4176
4382
|
if (type === "region") {
|
|
4177
|
-
const asset = assetFor(a.path ?? name);
|
|
4383
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4178
4384
|
if (!asset) continue;
|
|
4179
4385
|
attachments.push({
|
|
4180
4386
|
id,
|
|
@@ -4190,21 +4396,48 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4190
4396
|
...a.height ? { height: a.height } : {}
|
|
4191
4397
|
});
|
|
4192
4398
|
} else if (type === "mesh" || type === "linkedmesh") {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
}
|
|
4197
|
-
const asset = assetFor(a.path ?? name);
|
|
4399
|
+
const geometry = a.parent ? meshGeometry(skin2.name, slotName, name) : a;
|
|
4400
|
+
if (!geometry) continue;
|
|
4401
|
+
const asset = assetFor(a.path ?? a.name ?? name);
|
|
4198
4402
|
if (!asset) continue;
|
|
4199
|
-
const n = (
|
|
4200
|
-
attachments.push({ id, slot, type: "mesh", asset, uvs:
|
|
4403
|
+
const n = (geometry.uvs?.length ?? 0) / 2;
|
|
4404
|
+
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 } : {} });
|
|
4405
|
+
if (a.parent && (a.timelines ?? a.deform ?? true)) {
|
|
4406
|
+
const parentSkin = a.skin ?? "default";
|
|
4407
|
+
inheritedDeform.set(id, attachmentId(slotName, (parentSkin === "default" ? "" : `${parentSkin}_`) + a.parent));
|
|
4408
|
+
}
|
|
4201
4409
|
} else if (type === "path") {
|
|
4202
4410
|
attachments.push({ id, slot, type: "path", ...readVertices(a, a.vertexCount ?? 0), lengths: a.lengths ?? [], closed: !!a.closed, constantSpeed: a.constantSpeed ?? true });
|
|
4411
|
+
} else if (type === "clipping") {
|
|
4412
|
+
const data = readVertices(a, a.vertexCount), n = a.vertexCount;
|
|
4413
|
+
const recipes = Array.from({ length: n }, (_, i) => [
|
|
4414
|
+
[[(i + n - 1) % n, 1 / 3], [i, 2 / 3]],
|
|
4415
|
+
[[i, 1]],
|
|
4416
|
+
[[i, 2 / 3], [(i + 1) % n, 1 / 3]]
|
|
4417
|
+
]).flat();
|
|
4418
|
+
let geometry;
|
|
4419
|
+
if (data.weights) {
|
|
4420
|
+
const starts = [];
|
|
4421
|
+
let offset = 0;
|
|
4422
|
+
for (const list of data.weights) {
|
|
4423
|
+
starts.push(offset);
|
|
4424
|
+
offset += list.length * 2;
|
|
4425
|
+
}
|
|
4426
|
+
const mapping = recipes.flatMap((recipe) => recipe.flatMap(([index]) => data.weights[index].map((_, k) => starts[index] + k * 2)));
|
|
4427
|
+
geometry = { weights: recipes.map((recipe) => recipe.flatMap(([index, factor]) => data.weights[index].map((inf) => ({ ...inf, w: inf.w * factor })))) };
|
|
4428
|
+
clipDeforms.set(id, (offsets) => mapping.flatMap((index) => [offsets[index] ?? 0, offsets[index + 1] ?? 0]));
|
|
4429
|
+
} else {
|
|
4430
|
+
const expand = (values2) => recipes.flatMap((recipe) => [0, 1].map((axis) => recipe.reduce((sum, [index, factor]) => sum + (values2[index * 2 + axis] ?? 0) * factor, 0)));
|
|
4431
|
+
geometry = { vertices: expand(data.vertices) };
|
|
4432
|
+
clipDeforms.set(id, expand);
|
|
4433
|
+
}
|
|
4434
|
+
attachments.push({ id, slot, type: "path", ...geometry, closed: true, clipping: { ...a.end ? { end: slotIds.get(a.end) } : {} } });
|
|
4203
4435
|
} else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
|
|
4204
4436
|
}
|
|
4205
4437
|
}
|
|
4206
4438
|
}
|
|
4207
|
-
const
|
|
4439
|
+
const activeConstraint = (kind, c) => !c.skin || lookupSkins.some((s2) => s2[kind]?.includes(c.name));
|
|
4440
|
+
const ik = (json.ik ?? []).filter((c) => activeConstraint("ik", c)).map((c) => ({
|
|
4208
4441
|
id: ikIds.get(c.name),
|
|
4209
4442
|
order: c.order ?? 0,
|
|
4210
4443
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4217,7 +4450,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4217
4450
|
stretch: !!c.stretch,
|
|
4218
4451
|
uniform: !!c.uniform
|
|
4219
4452
|
}));
|
|
4220
|
-
const transform = (json.transform ?? []).map((c) => ({
|
|
4453
|
+
const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
|
|
4221
4454
|
id: tcIds.get(c.name),
|
|
4222
4455
|
order: c.order ?? 0,
|
|
4223
4456
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4237,7 +4470,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4237
4470
|
mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
|
|
4238
4471
|
mixShearY: c.mixShearY ?? c.shearMix ?? 1
|
|
4239
4472
|
}));
|
|
4240
|
-
const path = (json.path ?? []).map((c) => ({
|
|
4473
|
+
const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
|
|
4241
4474
|
id: pcIds.get(c.name),
|
|
4242
4475
|
order: c.order ?? 0,
|
|
4243
4476
|
bones: c.bones.map((b) => boneIds.get(b)),
|
|
@@ -4272,7 +4505,21 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4272
4505
|
for (let i = 0; i < frames.length; i++) {
|
|
4273
4506
|
const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
|
|
4274
4507
|
const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
|
|
4275
|
-
|
|
4508
|
+
const curve = major >= 4 && Array.isArray(f.curve) ? f.curve.slice(comp * 4, comp * 4 + 4) : [];
|
|
4509
|
+
const value = f[field] ?? def;
|
|
4510
|
+
if (next && value === (next[field] ?? def) && curve.length === 4 && (curve[1] !== value || curve[3] !== value)) {
|
|
4511
|
+
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;
|
|
4512
|
+
const inner = Array.from({ length: 9 }, (_, k) => {
|
|
4513
|
+
const u = (k + 1) / 10;
|
|
4514
|
+
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" };
|
|
4515
|
+
});
|
|
4516
|
+
if (inner.every((k, i2) => k.t > (i2 ? inner[i2 - 1].t : t) && k.t < (next.time ?? 0))) {
|
|
4517
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: "linear" }, ...inner);
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
warnings.push(`equal-endpoint ${field} curve at ${t}s has non-monotonic control times and could not be lowered`);
|
|
4521
|
+
}
|
|
4522
|
+
keys.push({ t: r4(t), v: r4(abs(value)), ease: ease2 });
|
|
4276
4523
|
}
|
|
4277
4524
|
return keys;
|
|
4278
4525
|
};
|
|
@@ -4283,6 +4530,10 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4283
4530
|
const seen = (frames) => {
|
|
4284
4531
|
for (const f of frames) duration = Math.max(duration, f.time ?? 0);
|
|
4285
4532
|
};
|
|
4533
|
+
if (a.physics) {
|
|
4534
|
+
warnings.push(`${animName}: Spine physics timelines are not imported`);
|
|
4535
|
+
for (const timelines of Object.values(a.physics)) for (const frames of Object.values(timelines)) seen(frames);
|
|
4536
|
+
}
|
|
4286
4537
|
for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
|
|
4287
4538
|
const sb = spineBones[boneIndexByName.get(boneName)];
|
|
4288
4539
|
if (!sb) {
|
|
@@ -4323,19 +4574,22 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4323
4574
|
if (kind === "attachment") {
|
|
4324
4575
|
attachmentFrames.set(slotName, frames);
|
|
4325
4576
|
tracks.push(track("slot", id, "attachment", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.name ? attachmentRef(slotName, f.name) : null, ease: "step" }))));
|
|
4326
|
-
} else if (
|
|
4577
|
+
} else if (["rgba", "color", "rgb", "rgba2", "rgb2"].includes(kind)) {
|
|
4578
|
+
const setupAlpha = parseInt(hex8(spineSlots[slotIndexByName.get(slotName)].color).slice(7), 16) / 255;
|
|
4579
|
+
const rgbOnly = kind === "rgb" || kind === "rgb2";
|
|
4327
4580
|
const rgbaOf = (f) => {
|
|
4328
|
-
const c = hex8(f.color);
|
|
4329
|
-
return [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4581
|
+
const c = hex8(f.color ?? f.light);
|
|
4582
|
+
return [0, 2, 4, 6].map((k, i) => i === 3 && rgbOnly ? setupAlpha : parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
4330
4583
|
};
|
|
4584
|
+
if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
|
|
4331
4585
|
tracks.push(track("slot", id, "color", frames.map((f, i) => {
|
|
4332
4586
|
const rgba = rgbaOf(f), next = frames[i + 1];
|
|
4333
|
-
return {
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4587
|
+
return {
|
|
4588
|
+
t: r4(f.time ?? 0),
|
|
4589
|
+
v: rgba.map(r4),
|
|
4590
|
+
ease: "linear",
|
|
4591
|
+
...next ? { componentEase: rgba.map((v, k) => rgbOnly && k === 3 ? "linear" : easeOf(f.curve, k, f.time ?? 0, next.time ?? 0, v, rgbaOf(next)[k])) } : {}
|
|
4592
|
+
};
|
|
4339
4593
|
})));
|
|
4340
4594
|
} else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
|
|
4341
4595
|
else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
|
|
@@ -4374,15 +4628,30 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4374
4628
|
warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
|
|
4375
4629
|
continue;
|
|
4376
4630
|
}
|
|
4377
|
-
const
|
|
4631
|
+
const rawClip = skins.find((s2) => s2.name === skinName)?.attachments?.[slotName]?.[attName];
|
|
4632
|
+
const clipData = clipDeforms.has(id) ? readVertices(rawClip, rawClip.vertexCount) : void 0;
|
|
4633
|
+
const data = clipData ?? att;
|
|
4634
|
+
const length = data.weights ? 2 * data.weights.reduce((s2, l) => s2 + l.length, 0) : data.vertices?.length ?? 0;
|
|
4378
4635
|
tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
|
|
4379
4636
|
const full = new Array(length).fill(0);
|
|
4380
4637
|
const off = f.offset ?? 0, vs = f.vertices ?? [];
|
|
4381
4638
|
for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
|
|
4382
4639
|
const next = frames[i + 1];
|
|
4383
|
-
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" };
|
|
4640
|
+
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" };
|
|
4384
4641
|
})));
|
|
4385
4642
|
}
|
|
4643
|
+
for (let pass = 0; pass < inheritedDeform.size; pass++) {
|
|
4644
|
+
let changed = false;
|
|
4645
|
+
for (const [child, parent] of inheritedDeform) {
|
|
4646
|
+
if (tracks.some((t) => t.target === "deform" && t.id === child)) continue;
|
|
4647
|
+
const source = tracks.find((t) => t.target === "deform" && t.id === parent);
|
|
4648
|
+
if (source) {
|
|
4649
|
+
tracks.push({ ...source, id: child, keys: structuredClone(source.keys) });
|
|
4650
|
+
changed = true;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (!changed) break;
|
|
4654
|
+
}
|
|
4386
4655
|
const drawOrderFrames = a.drawOrder ?? a.draworder;
|
|
4387
4656
|
if (Array.isArray(drawOrderFrames)) {
|
|
4388
4657
|
seen(drawOrderFrames);
|
|
@@ -4405,20 +4674,25 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4405
4674
|
for (const [name, frames] of Object.entries(a.ik ?? {})) {
|
|
4406
4675
|
seen(frames);
|
|
4407
4676
|
const id = ikIds.get(name);
|
|
4677
|
+
if (!ik.some((c) => c.id === id)) continue;
|
|
4408
4678
|
tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
|
|
4409
4679
|
tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
|
|
4410
4680
|
tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
|
|
4411
4681
|
tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
|
|
4412
4682
|
tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
|
|
4413
4683
|
}
|
|
4414
|
-
const mixTrack = (target, id, frames, prop, old, comp) =>
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4684
|
+
const mixTrack = (target, id, frames, prop, old, comp) => {
|
|
4685
|
+
const value = (f) => f[prop] ?? (prop === "mixY" ? f.mixX : prop === "mixScaleY" ? f.mixScaleX : void 0) ?? f[old] ?? 1;
|
|
4686
|
+
return track(target, id, prop, frames.map((f, i) => ({
|
|
4687
|
+
t: r4(f.time ?? 0),
|
|
4688
|
+
v: r4(value(f)),
|
|
4689
|
+
ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, value(f), value(frames[i + 1])) : "linear"
|
|
4690
|
+
})));
|
|
4691
|
+
};
|
|
4419
4692
|
for (const [name, frames] of Object.entries(a.transform ?? {})) {
|
|
4420
4693
|
seen(frames);
|
|
4421
4694
|
const id = tcIds.get(name);
|
|
4695
|
+
if (!transform.some((c) => c.id === id)) continue;
|
|
4422
4696
|
const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
|
|
4423
4697
|
for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
|
|
4424
4698
|
}
|
|
@@ -4426,6 +4700,7 @@ function importSpine(json, atlasText, opts = {}) {
|
|
|
4426
4700
|
const id = pcIds.get(name);
|
|
4427
4701
|
for (const [kind, frames] of Object.entries(tl)) {
|
|
4428
4702
|
seen(frames);
|
|
4703
|
+
if (!path.some((c) => c.id === id)) continue;
|
|
4429
4704
|
if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
|
|
4430
4705
|
else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
|
|
4431
4706
|
tracks.push(mixTrack("path", id, frames, prop, old, comp));
|
|
@@ -4588,21 +4863,27 @@ async function importSpineProject(opts) {
|
|
|
4588
4863
|
const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
|
|
4589
4864
|
const atlasText = await readFile7(atlasPath, "utf8");
|
|
4590
4865
|
const { pages } = parseAtlas(atlasText);
|
|
4591
|
-
if (pages.length
|
|
4592
|
-
|
|
4593
|
-
const imagePath = opts.image ?? await firstExisting([join8(dirname6(atlasPath), pages[0].name), pageStem + ".png", pageStem + ".ktx2", stem + ".png", stem + ".ktx2"], "atlas page image");
|
|
4866
|
+
if (!pages.length) throw new Error("atlas has no pages");
|
|
4867
|
+
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");
|
|
4594
4868
|
const rigDir = dirname6(opts.rigPath);
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4869
|
+
const pagePaths = /* @__PURE__ */ new Map();
|
|
4870
|
+
for (const page2 of pages) {
|
|
4871
|
+
const pageStem = join8(dirname6(atlasPath), page2.name.replace(/\.[a-z0-9]+$/i, ""));
|
|
4872
|
+
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}`);
|
|
4873
|
+
let pngPath = imagePath;
|
|
4874
|
+
if (extname(imagePath).toLowerCase() === ".ktx2") {
|
|
4875
|
+
pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
|
|
4876
|
+
if (!await exists(pngPath)) {
|
|
4877
|
+
const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
|
|
4878
|
+
await writeFile5(pngPath, png);
|
|
4879
|
+
}
|
|
4601
4880
|
}
|
|
4881
|
+
pagePaths.set(page2.name, pngPath);
|
|
4602
4882
|
}
|
|
4603
|
-
const pageImage = relative(rigDir,
|
|
4883
|
+
const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
|
|
4604
4884
|
const name = opts.name ?? basename4(stem);
|
|
4605
|
-
|
|
4885
|
+
const imagePaths = [...pagePaths.values()];
|
|
4886
|
+
return { ...importSpine(json, atlasText, { name, skin: opts.skin, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: imagePaths[0], imagePaths };
|
|
4606
4887
|
}
|
|
4607
4888
|
|
|
4608
4889
|
// src/rig-templates.ts
|
|
@@ -4619,7 +4900,7 @@ function extractTemplate(doc, animId, name, groups = {}) {
|
|
|
4619
4900
|
const tracks = [];
|
|
4620
4901
|
const skipped = [];
|
|
4621
4902
|
for (const t of anim.tracks) {
|
|
4622
|
-
if (t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4903
|
+
if (t.target === "control" || t.target === "drawOrder" || t.keys.some((k) => Array.isArray(k.v))) {
|
|
4623
4904
|
skipped.push(trackId5(t));
|
|
4624
4905
|
continue;
|
|
4625
4906
|
}
|
|
@@ -4994,8 +5275,8 @@ function runGates(doc, config) {
|
|
|
4994
5275
|
let active2 = 0, bad = 0;
|
|
4995
5276
|
for (const t of ts) {
|
|
4996
5277
|
const pose = poseOf(animation, t), s2 = evaluate(doc, pose);
|
|
4997
|
-
const color = pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
4998
|
-
const alpha = Number(pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
5278
|
+
const color = s2.pose.get(pk("slot", slot.id, "color")) ?? colorToArray(slot.color);
|
|
5279
|
+
const alpha = Number(s2.pose.get(pk("slot", slot.id, "alpha")) ?? slot.alpha) * color[3];
|
|
4999
5280
|
if (!s2.attachment.get(slot.id) || alpha <= 0.01) continue;
|
|
5000
5281
|
active2++;
|
|
5001
5282
|
const v = bone(s2, rule.bone, rule.prop === "rotation" ? "rotation" : "position", rule.space)[rule.prop === "y" ? 1 : 0];
|
|
@@ -5135,13 +5416,14 @@ function attachToBone(doc, animation, bone, target, t) {
|
|
|
5135
5416
|
if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
|
|
5136
5417
|
const [x, y] = apply(invert(to), from.tx, from.ty);
|
|
5137
5418
|
const id = `attach_${bone}_${target}`;
|
|
5419
|
+
const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
|
|
5138
5420
|
const offset = {
|
|
5139
5421
|
x,
|
|
5140
5422
|
y,
|
|
5141
|
-
rotation: wrap2(rotationOf(from) - rotationOf(to)),
|
|
5423
|
+
rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
|
|
5142
5424
|
scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
|
|
5143
5425
|
scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
|
|
5144
|
-
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)
|
|
5426
|
+
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
|
|
5145
5427
|
};
|
|
5146
5428
|
const order = Math.max(0, ...doc.ik.map((c) => c.order), ...doc.transform.map((c) => c.order), ...doc.path.map((c) => c.order)) + 1;
|
|
5147
5429
|
let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
|
|
@@ -5217,6 +5499,53 @@ function bakeSecondary(doc, animation, opts) {
|
|
|
5217
5499
|
return next;
|
|
5218
5500
|
}
|
|
5219
5501
|
|
|
5502
|
+
// src/rig-control-qa.ts
|
|
5503
|
+
function snapshot(doc, values2) {
|
|
5504
|
+
const skel = evaluate(doc, controlPose(doc, values2));
|
|
5505
|
+
return doc.slots.flatMap((slot) => {
|
|
5506
|
+
const alpha = Number(skel.pose.get(`slot:${slot.id}:alpha`) ?? slot.alpha);
|
|
5507
|
+
const points = slotOutline(doc, skel.pose, slot.id, 1, skel);
|
|
5508
|
+
return points ? [{ id: slot.id, alpha, points }] : [];
|
|
5509
|
+
});
|
|
5510
|
+
}
|
|
5511
|
+
function checkControlCases(doc, cases) {
|
|
5512
|
+
const errors = [];
|
|
5513
|
+
const rest = evaluate(doc, /* @__PURE__ */ new Map());
|
|
5514
|
+
cases.forEach((c, index) => {
|
|
5515
|
+
const skel = evaluate(doc, controlPose(doc, c.values));
|
|
5516
|
+
for (const [id, b] of skel.bones) if (Object.values(b.world).some((v) => !Number.isFinite(v)))
|
|
5517
|
+
errors.push({ case: index, subject: id, message: "non-finite bone transform" });
|
|
5518
|
+
for (const att of doc.attachments) if (att.type === "mesh") {
|
|
5519
|
+
const before = attachmentWorldVertices(rest, att.slot, att), after = attachmentWorldVertices(skel, att.slot, att);
|
|
5520
|
+
if (after.some((v) => !Number.isFinite(v))) errors.push({ case: index, subject: att.id, message: "non-finite mesh vertices" });
|
|
5521
|
+
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))
|
|
5522
|
+
errors.push({ case: index, subject: att.id, message: "mesh triangle reversed or collapsed" });
|
|
5523
|
+
}
|
|
5524
|
+
});
|
|
5525
|
+
const neutral = JSON.stringify(snapshot(doc, {}));
|
|
5526
|
+
const effects = (doc.controls ?? []).map((c) => ({
|
|
5527
|
+
id: c.id,
|
|
5528
|
+
geometryOrOpacityChanged: [c.min, c.max].some((v) => JSON.stringify(snapshot(doc, { [c.id]: v })) !== neutral)
|
|
5529
|
+
}));
|
|
5530
|
+
return {
|
|
5531
|
+
passed: errors.length === 0,
|
|
5532
|
+
errors,
|
|
5533
|
+
effects,
|
|
5534
|
+
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."
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
function makeControlSweep(doc, probes) {
|
|
5538
|
+
const cases = controlCases(doc, probes).map((c) => ({ ...c, values: { ...Object.fromEntries((doc.controls ?? []).map((p) => [p.id, p.default])), ...c.values } }));
|
|
5539
|
+
const animation = {
|
|
5540
|
+
id: "control_sweep",
|
|
5541
|
+
duration: Math.max(1, cases.length - 1),
|
|
5542
|
+
loop: false,
|
|
5543
|
+
events: [],
|
|
5544
|
+
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" })) }))
|
|
5545
|
+
};
|
|
5546
|
+
return { cases, doc: parse({ ...doc, animations: [animation] }), animation: animation.id, times: cases.map((_, i) => i) };
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5220
5549
|
// src/rig-api.ts
|
|
5221
5550
|
var tools = [];
|
|
5222
5551
|
function tool(name, description, input, run) {
|
|
@@ -5280,14 +5609,88 @@ function previewCheck(doc, animation) {
|
|
|
5280
5609
|
const a = animation ? doc.animations.find((x) => x.id === animation) : void 0;
|
|
5281
5610
|
return a ? `check: OK for "${a.id}" (${a.tracks.length} tracks, ${a.duration}s${a.loop ? " loop" : ""})` : "check: OK (rest pose)";
|
|
5282
5611
|
}
|
|
5283
|
-
var TrackRef = { interpolation: z3.enum(["segment", "pchip"]).optional().describe("pchip: smooth through numeric keys, preserving extrema; segment: per-key easing"), target: z3.enum(["bone", "slot", "deform", "ik", "transform", "path", "drawOrder"]), id: z3.string(), prop: z3.string() };
|
|
5612
|
+
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() };
|
|
5613
|
+
tool(
|
|
5614
|
+
"rig_set_control",
|
|
5615
|
+
"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.",
|
|
5616
|
+
{ path: Path, control: RigControl },
|
|
5617
|
+
async ({ path, control }) => mutate(path, (d) => ({ ...d, controls: [...(d.controls ?? []).filter((c) => c.id !== control.id), control] }))
|
|
5618
|
+
);
|
|
5619
|
+
tool(
|
|
5620
|
+
"rig_set_control_binding",
|
|
5621
|
+
"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.",
|
|
5622
|
+
{ path: Path, binding: ControlBinding },
|
|
5623
|
+
async ({ path, binding }) => mutate(path, (d) => ({ ...d, controlBindings: [...(d.controlBindings ?? []).filter((b) => b.id !== binding.id), binding] }))
|
|
5624
|
+
);
|
|
5625
|
+
tool(
|
|
5626
|
+
"rig_remove_control_binding",
|
|
5627
|
+
"Remove a control binding, restoring its channels to setup/animation values.",
|
|
5628
|
+
{ path: Path, id: z3.string() },
|
|
5629
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5630
|
+
if (!d.controlBindings?.some((b) => b.id === id)) throw new Error(`unknown binding "${id}"`);
|
|
5631
|
+
return { ...d, controlBindings: d.controlBindings.filter((b) => b.id !== id) };
|
|
5632
|
+
})
|
|
5633
|
+
);
|
|
5634
|
+
tool(
|
|
5635
|
+
"rig_remove_control",
|
|
5636
|
+
"Remove an unused control and its animation tracks. Remove dependent bindings first.",
|
|
5637
|
+
{ path: Path, id: z3.string() },
|
|
5638
|
+
async ({ path, id }) => mutate(path, (d) => {
|
|
5639
|
+
if (!d.controls?.some((c) => c.id === id)) throw new Error(`unknown control "${id}"`);
|
|
5640
|
+
if (d.controlBindings?.some((b) => b.axes.some((a) => a.control === id))) throw new Error("remove dependent control bindings first");
|
|
5641
|
+
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) })) };
|
|
5642
|
+
})
|
|
5643
|
+
);
|
|
5644
|
+
tool(
|
|
5645
|
+
"rig_sweep_controls",
|
|
5646
|
+
"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.",
|
|
5647
|
+
{
|
|
5648
|
+
path: Path,
|
|
5649
|
+
probes: z3.record(z3.array(z3.number().finite()).min(1)),
|
|
5650
|
+
output: z3.string(),
|
|
5651
|
+
scale: z3.number().positive().max(2).default(0.5),
|
|
5652
|
+
region: z3.tuple([z3.number(), z3.number(), z3.number().positive(), z3.number().positive()]).optional()
|
|
5653
|
+
},
|
|
5654
|
+
async ({ path, probes, output, scale, region }) => {
|
|
5655
|
+
try {
|
|
5656
|
+
const raw = await readFile9(path, "utf8"), d = parse(JSON.parse(raw));
|
|
5657
|
+
const sweep = makeControlSweep(d, probes), out = resolveFrom(path, output);
|
|
5658
|
+
if (await exists2(out) && (await readdir4(out)).length) throw new Error("sweep output must be a new or empty directory");
|
|
5659
|
+
const qa = checkControlCases(d, sweep.cases);
|
|
5660
|
+
const r = await renderPreview(sweep.doc, { assetsDir: dirname8(path), animation: sweep.animation, times: sweep.times, scale, region, transparent: true });
|
|
5661
|
+
await mkdir6(out, { recursive: true });
|
|
5662
|
+
const frames = [];
|
|
5663
|
+
for (let i = 0; i < r.frames.length; i++) {
|
|
5664
|
+
const file = `pose-${String(i).padStart(3, "0")}.png`;
|
|
5665
|
+
await writeFile7(join9(out, file), r.frames[i]);
|
|
5666
|
+
frames.push({ file, sha256: sha256(r.frames[i]), ...sweep.cases[i] });
|
|
5667
|
+
}
|
|
5668
|
+
await writeFile7(join9(out, "sheet.png"), r.sheet);
|
|
5669
|
+
const report = {
|
|
5670
|
+
version: 1,
|
|
5671
|
+
sourceRig: sha256(raw),
|
|
5672
|
+
identity: r.identity,
|
|
5673
|
+
scale,
|
|
5674
|
+
region: region ?? null,
|
|
5675
|
+
...qa,
|
|
5676
|
+
frames,
|
|
5677
|
+
sheet: { file: "sheet.png", sha256: sha256(r.sheet) }
|
|
5678
|
+
};
|
|
5679
|
+
await saveJson(join9(out, "report.json"), report);
|
|
5680
|
+
return { text: JSON.stringify({ output: out, poseCount: frames.length, ...qa }), image: r.sheet };
|
|
5681
|
+
} catch (e) {
|
|
5682
|
+
return fail(e);
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
);
|
|
5284
5686
|
var KeyIn = z3.object({
|
|
5285
5687
|
t: z3.number().describe("seconds"),
|
|
5286
5688
|
v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
|
|
5689
|
+
componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
|
|
5287
5690
|
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")
|
|
5288
5691
|
});
|
|
5289
5692
|
var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
|
|
5290
|
-
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].";
|
|
5693
|
+
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].";
|
|
5291
5694
|
tool(
|
|
5292
5695
|
"rig_create",
|
|
5293
5696
|
"Create a new empty rig document with a root bone at bottom-center.",
|
|
@@ -5411,19 +5814,20 @@ tool(
|
|
|
5411
5814
|
);
|
|
5412
5815
|
tool(
|
|
5413
5816
|
"rig_import_spine",
|
|
5414
|
-
"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;
|
|
5817
|
+
"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.",
|
|
5415
5818
|
{
|
|
5416
5819
|
path: Path,
|
|
5417
5820
|
json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
|
|
5418
5821
|
atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
|
|
5419
5822
|
image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
|
|
5823
|
+
skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
|
|
5420
5824
|
name: z3.string().optional().describe("default: the json file name"),
|
|
5421
5825
|
margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
|
|
5422
5826
|
notes: z3.string().optional()
|
|
5423
5827
|
},
|
|
5424
|
-
async ({ path, json, atlas, image, name, margin, notes }) => {
|
|
5828
|
+
async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
|
|
5425
5829
|
try {
|
|
5426
|
-
const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
|
|
5830
|
+
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 });
|
|
5427
5831
|
const res = await create(path, parse(r.doc));
|
|
5428
5832
|
if (r.warnings.length) res.text += `
|
|
5429
5833
|
warnings:
|
|
@@ -6112,8 +6516,8 @@ tool(
|
|
|
6112
6516
|
const r = await renderPreview(doc, { assetsDir: dirname8(path), ...opts });
|
|
6113
6517
|
if (saveTo) await writeFile7(saveTo, r.sheet);
|
|
6114
6518
|
if (framesDir) {
|
|
6115
|
-
const { mkdir:
|
|
6116
|
-
await
|
|
6519
|
+
const { mkdir: mkdir8 } = await import("node:fs/promises");
|
|
6520
|
+
await mkdir8(framesDir, { recursive: true });
|
|
6117
6521
|
const frames = r.frames.map((frame, i) => ({ file: `${String(i).padStart(6, "0")}.png`, sha256: sha256(frame) }));
|
|
6118
6522
|
for (let i = 0; i < frames.length; i++) await writeFile7(join9(framesDir, frames[i].file), r.frames[i]);
|
|
6119
6523
|
const manifest = {
|
|
@@ -6263,7 +6667,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6263
6667
|
async function listRigs() {
|
|
6264
6668
|
const out = [];
|
|
6265
6669
|
const walk = async (dir) => {
|
|
6266
|
-
for (const e of await
|
|
6670
|
+
for (const e of await readdir5(dir, { withFileTypes: true })) {
|
|
6267
6671
|
if (e.isDirectory()) {
|
|
6268
6672
|
if (!SKIP.has(e.name)) await walk(join10(dir, e.name));
|
|
6269
6673
|
continue;
|
|
@@ -6283,7 +6687,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6283
6687
|
}
|
|
6284
6688
|
async function listFiles(rigAbs) {
|
|
6285
6689
|
const dir = dirname9(rigAbs), out = [];
|
|
6286
|
-
for (const e of (await
|
|
6690
|
+
for (const e of (await readdir5(dir, { withFileTypes: true })).filter((e2) => e2.isFile() && /\.png$/i.test(e2.name)).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6287
6691
|
const fh = await open(join10(dir, e.name));
|
|
6288
6692
|
try {
|
|
6289
6693
|
const head = Buffer.alloc(24);
|
|
@@ -6401,7 +6805,7 @@ data: ${JSON.stringify({ version: v })}
|
|
|
6401
6805
|
const abs = inside(rig);
|
|
6402
6806
|
if (!abs) return json(res, 403, { isError: true, text: "rig outside root" });
|
|
6403
6807
|
if (await stat3(abs).then(() => true, () => false)) return json(res, 409, { isError: true, text: `ERROR: ${rig} already exists \u2014 open it, or pick another directory` });
|
|
6404
|
-
await
|
|
6808
|
+
await mkdir7(dirname9(abs), { recursive: true });
|
|
6405
6809
|
const r = await callTool("rig_create", { path: abs, name: name ?? basename5(dirname9(abs)), width: width ?? 640, height: height ?? 700 });
|
|
6406
6810
|
ensureWatch(abs);
|
|
6407
6811
|
return json(res, r.isError ? 400 : 200, { text: r.text, doc: r.doc, isError: r.isError, version: await version(abs) });
|