@energy8platform/golem 0.5.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.
@@ -26,6 +26,8 @@ export declare function parseAtlas(text: string): {
26
26
  };
27
27
  export interface ImportOptions {
28
28
  name?: string;
29
+ /** Flatten this skin into setup/attachment timelines, falling back only to the default skin. */
30
+ skin?: string;
29
31
  /** file name to use for the atlas page image(s) in asset.src (e.g. "ross.png" after transcoding a ktx2) */
30
32
  pageImage?: string | ((page: string) => string);
31
33
  /** canvas margin around the skeleton bounds, px */
@@ -58,11 +60,13 @@ export interface SpineProjectOptions {
58
60
  /** where the rig will live: the page asset is referenced relative to its directory (KTX2 pages are written there as PNG) */
59
61
  rigPath: string;
60
62
  name?: string;
63
+ skin?: string;
61
64
  margin?: number;
62
65
  notes?: string;
63
66
  }
64
67
  /** Import a Spine project from files. Resolves the atlas and page image, transcodes KTX2 pages to PNG next to the rig. */
65
68
  export declare function importSpineProject(opts: SpineProjectOptions): Promise<ReturnType<typeof importSpine> & {
66
69
  imagePath: string;
70
+ imagePaths: string[];
67
71
  }>;
68
72
  export {};
package/dist/lib/tools.js CHANGED
@@ -216,7 +216,9 @@ var PathAttachment = z.object({
216
216
  /** curve lengths for constant-speed positioning */
217
217
  lengths: z.array(z.number()).default([]),
218
218
  closed: z.boolean().default(false),
219
- constantSpeed: z.boolean().default(true)
219
+ constantSpeed: z.boolean().default(true),
220
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
221
+ clipping: z.object({ end: Id.optional() }).optional()
220
222
  });
221
223
  var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
222
224
  var IkConstraint = z.object({
@@ -285,7 +287,9 @@ var Key = z.object({
285
287
  t: z.number().nonnegative(),
286
288
  v: KeyValue,
287
289
  /** easing INTO the next key */
288
- ease: Ease.default("ease_in_out")
290
+ ease: Ease.default("ease_in_out"),
291
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
292
+ componentEase: z.array(Ease).min(1).optional()
289
293
  });
290
294
  var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
291
295
  var Track = z.discriminatedUnion("target", [
@@ -472,6 +476,10 @@ function validate(doc) {
472
476
  else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
473
477
  }
474
478
  }
479
+ for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
480
+ if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
481
+ if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
482
+ }
475
483
  const vertexCount2 = (a) => {
476
484
  if (a.type === "region") return 4;
477
485
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -555,6 +563,7 @@ function validate(doc) {
555
563
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
556
564
  let prev = -1;
557
565
  for (const k of t.keys) {
566
+ if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
558
567
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
559
568
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
560
569
  if (t.target === "control") {
@@ -732,8 +741,8 @@ function sample(track, t, loop = false) {
732
741
  }
733
742
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
734
743
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
735
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
736
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
744
+ const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
745
+ return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
737
746
  }
738
747
  return a.v;
739
748
  }
@@ -825,6 +834,13 @@ function boneWorldOf(bone, s, parent) {
825
834
  if (!parent) return local;
826
835
  if (bone.inherit === "normal") return mul(parent, local);
827
836
  const [tx, ty] = apply(parent, s.x, s.y);
837
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
838
+ const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
839
+ const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
840
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
841
+ const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
842
+ return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
843
+ }
828
844
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
829
845
  }
830
846
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -857,11 +873,11 @@ var localOf = (doc, pose, b) => {
857
873
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
858
874
  };
859
875
  };
860
- function updateSubtree(skel, root2) {
876
+ function updateSubtree(skel, root2, includeRoot = true) {
861
877
  const doc = skel.doc;
862
878
  const kids = /* @__PURE__ */ new Map();
863
879
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
864
- const stack = [root2];
880
+ const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
865
881
  while (stack.length) {
866
882
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
867
883
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
@@ -1104,7 +1120,8 @@ function applyTransformConstraint(skel, c) {
1104
1120
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1105
1121
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
1106
1122
  const target = skel.bones.get(c.target), tw = target.world;
1107
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
1123
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
1124
+ const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
1108
1125
  for (const id of c.bones) {
1109
1126
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
1110
1127
  if (c.local) {
@@ -1266,6 +1283,7 @@ function applyPathConstraint(skel, c) {
1266
1283
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1267
1284
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1268
1285
  const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
1286
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
1269
1287
  const spacesCount = tangents ? bones.length : bones.length + 1;
1270
1288
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1271
1289
  let sum = 0;
@@ -1300,9 +1318,10 @@ function applyPathConstraint(skel, c) {
1300
1318
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1301
1319
  }
1302
1320
  let boneX = samples[0].x, boneY = samples[0].y;
1321
+ const solvedWorlds = /* @__PURE__ */ new Map();
1303
1322
  for (let i = 0; i < bones.length; i++) {
1304
1323
  const { b, st } = bones[i];
1305
- let w = { ...st.world };
1324
+ let w = { ...inputWorlds[i] };
1306
1325
  w.tx += (boneX - w.tx) * mixX;
1307
1326
  w.ty += (boneY - w.ty) * mixY;
1308
1327
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1332,7 +1351,11 @@ function applyPathConstraint(skel, c) {
1332
1351
  const cos = Math.cos(r), sin = Math.sin(r);
1333
1352
  w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
1334
1353
  }
1335
- setEffectiveWorld(skel, b, st, w);
1354
+ solvedWorlds.set(b.id, w);
1355
+ }
1356
+ for (const b of boneOrder(skel.doc)) {
1357
+ const world = solvedWorlds.get(b.id);
1358
+ if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
1336
1359
  }
1337
1360
  }
1338
1361
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1361,9 +1384,14 @@ function inheritedParentLinear(bone, parent) {
1361
1384
  case "onlyTranslation":
1362
1385
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1363
1386
  case "noRotationOrReflection": {
1364
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1387
+ const lenX = Math.hypot(parent.a, parent.b);
1365
1388
  const det = parent.a * parent.d - parent.b * parent.c;
1366
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
1389
+ if (lenX * lenX <= 1e-4) {
1390
+ const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
1391
+ return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
1392
+ }
1393
+ const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
1394
+ return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
1367
1395
  }
1368
1396
  case "noScale":
1369
1397
  case "noScaleOrReflection": {
@@ -1389,7 +1417,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1389
1417
  st.scaleY = Math.hypot(lin.c, lin.d);
1390
1418
  st.shearX = 0;
1391
1419
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1392
- updateSubtree(skel, bone.id);
1420
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
1421
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
1422
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
1423
+ st.scaleX = Math.hypot(world.a, world.b);
1424
+ st.scaleY = Math.hypot(world.c, world.d);
1425
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
1426
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
1427
+ }
1428
+ st.world = world;
1429
+ updateSubtree(skel, bone.id, false);
1393
1430
  }
1394
1431
  function attachmentWorldVertices(skel, slotId, att) {
1395
1432
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -2148,7 +2185,7 @@ function removeSlot(doc, id) {
2148
2185
  return patchDrawOrder({
2149
2186
  ...doc,
2150
2187
  slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
2151
- attachments: doc.attachments.filter((a) => a.slot !== id),
2188
+ attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
2152
2189
  animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
2153
2190
  }, (o) => o.filter((s) => s !== id));
2154
2191
  }
@@ -2544,7 +2581,7 @@ function rename2(doc, kind, id, to) {
2544
2581
  return {
2545
2582
  ...doc,
2546
2583
  slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
2547
- attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
2584
+ attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
2548
2585
  path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
2549
2586
  };
2550
2587
  }
@@ -4157,6 +4194,9 @@ var Ids = class {
4157
4194
  has(name) {
4158
4195
  return this.map.has(name);
4159
4196
  }
4197
+ reserve(id) {
4198
+ this.used.add(id);
4199
+ }
4160
4200
  };
4161
4201
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4162
4202
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
@@ -4165,6 +4205,7 @@ function importSpine(json, atlasText, opts = {}) {
4165
4205
  const warnings = [];
4166
4206
  const atlas = parseAtlas(atlasText);
4167
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`);
4168
4209
  const major = parseFloat(version);
4169
4210
  const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4170
4211
  const margin = opts.margin ?? 0;
@@ -4172,6 +4213,7 @@ function importSpine(json, atlasText, opts = {}) {
4172
4213
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
4173
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_");
4174
4215
  const ROOT = "origin";
4216
+ boneIds.reserve(ROOT);
4175
4217
  const attachmentIdMap = /* @__PURE__ */ new Map();
4176
4218
  const attachmentId = (slot, name) => {
4177
4219
  const key = `${slot} ${name}`;
@@ -4196,13 +4238,17 @@ function importSpine(json, atlasText, opts = {}) {
4196
4238
  });
4197
4239
  }
4198
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)`);
4199
4245
  const frameSuffix = (seq, index) => {
4200
- const n = String((seq.start ?? 0) + index);
4246
+ const n = String((seq.start ?? 1) + index);
4201
4247
  return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
4202
4248
  };
4203
4249
  const sequences = /* @__PURE__ */ new Map();
4204
4250
  const skinnedName = /* @__PURE__ */ new Map();
4205
- for (const skin2 of skins) {
4251
+ for (const skin2 of lookupSkins) {
4206
4252
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4207
4253
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
4208
4254
  for (const [name, a] of Object.entries(atts)) {
@@ -4211,7 +4257,10 @@ function importSpine(json, atlasText, opts = {}) {
4211
4257
  if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
4212
4258
  }
4213
4259
  }
4214
- const attachmentRef = (slot, name) => attachmentId(slot, skinnedName.get(`${slot} ${name}`) ?? name);
4260
+ const attachmentRef = (slot, name) => {
4261
+ const resolved = skinnedName.get(`${slot} ${name}`);
4262
+ return resolved === void 0 ? null : attachmentId(slot, resolved);
4263
+ };
4215
4264
  const spineSlots = json.slots ?? [];
4216
4265
  const slotIndexByName = new Map(spineSlots.map((s, i) => [s.name, i]));
4217
4266
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
@@ -4219,11 +4268,12 @@ function importSpine(json, atlasText, opts = {}) {
4219
4268
  id: slotIds.get(s.name),
4220
4269
  bone: boneIds.get(s.bone),
4221
4270
  z: i,
4222
- ...s.attachment ? { attachment: attachmentRef(s.name, s.attachment) } : {},
4271
+ ...s.attachment && attachmentRef(s.name, s.attachment) ? { attachment: attachmentRef(s.name, s.attachment) } : {},
4223
4272
  blend: blendOf(s.blend),
4224
4273
  color: hex8(s.color),
4225
4274
  alpha: 1
4226
4275
  }));
4276
+ if (spineSlots.some((s) => s.dark)) warnings.push("Spine setup dark two-color tint is not supported");
4227
4277
  const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
4228
4278
  const assets = /* @__PURE__ */ new Map();
4229
4279
  const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
@@ -4249,6 +4299,7 @@ function importSpine(json, atlasText, opts = {}) {
4249
4299
  return id;
4250
4300
  };
4251
4301
  const attachments = [];
4302
+ const clipDeforms = /* @__PURE__ */ new Map();
4252
4303
  const readVertices = (a, vertexCount2) => {
4253
4304
  const v = a.vertices ?? [];
4254
4305
  if (v.length === vertexCount2 * 2) {
@@ -4269,6 +4320,22 @@ function importSpine(json, atlasText, opts = {}) {
4269
4320
  }
4270
4321
  return { weights };
4271
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();
4272
4339
  for (const skin2 of skins) {
4273
4340
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4274
4341
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
@@ -4280,7 +4347,7 @@ function importSpine(json, atlasText, opts = {}) {
4280
4347
  for (const [name, a] of Object.entries(atts)) {
4281
4348
  if (a.sequence) {
4282
4349
  sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
4283
- const base = a.path ?? name;
4350
+ const base = a.path ?? a.name ?? name;
4284
4351
  for (let i = 0; i < (a.sequence.count ?? 0); i++) {
4285
4352
  const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
4286
4353
  if (!asset) continue;
@@ -4303,7 +4370,7 @@ function importSpine(json, atlasText, opts = {}) {
4303
4370
  }
4304
4371
  const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
4305
4372
  if (type === "region") {
4306
- const asset = assetFor(a.path ?? name);
4373
+ const asset = assetFor(a.path ?? a.name ?? name);
4307
4374
  if (!asset) continue;
4308
4375
  attachments.push({
4309
4376
  id,
@@ -4319,21 +4386,48 @@ function importSpine(json, atlasText, opts = {}) {
4319
4386
  ...a.height ? { height: a.height } : {}
4320
4387
  });
4321
4388
  } else if (type === "mesh" || type === "linkedmesh") {
4322
- if (type === "linkedmesh") {
4323
- warnings.push(`linked mesh "${slotName}/${name}" skipped`);
4324
- continue;
4325
- }
4326
- 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);
4327
4392
  if (!asset) continue;
4328
- const n = (a.uvs?.length ?? 0) / 2;
4329
- attachments.push({ id, slot, type: "mesh", asset, uvs: a.uvs, triangles: a.triangles, ...readVertices(a, n), hull: a.hull ?? 0, ...a.width ? { width: a.width } : {}, ...a.height ? { height: a.height } : {} });
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
+ }
4330
4399
  } else if (type === "path") {
4331
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) } : {} } });
4332
4425
  } else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
4333
4426
  }
4334
4427
  }
4335
4428
  }
4336
- const ik = (json.ik ?? []).map((c) => ({
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) => ({
4337
4431
  id: ikIds.get(c.name),
4338
4432
  order: c.order ?? 0,
4339
4433
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4346,7 +4440,7 @@ function importSpine(json, atlasText, opts = {}) {
4346
4440
  stretch: !!c.stretch,
4347
4441
  uniform: !!c.uniform
4348
4442
  }));
4349
- const transform = (json.transform ?? []).map((c) => ({
4443
+ const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4350
4444
  id: tcIds.get(c.name),
4351
4445
  order: c.order ?? 0,
4352
4446
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4366,7 +4460,7 @@ function importSpine(json, atlasText, opts = {}) {
4366
4460
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4367
4461
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4368
4462
  }));
4369
- const path = (json.path ?? []).map((c) => ({
4463
+ const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4370
4464
  id: pcIds.get(c.name),
4371
4465
  order: c.order ?? 0,
4372
4466
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4401,7 +4495,21 @@ function importSpine(json, atlasText, opts = {}) {
4401
4495
  for (let i = 0; i < frames.length; i++) {
4402
4496
  const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
4403
4497
  const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
4404
- keys.push({ t: r4(t), v: r4(abs(f[field] ?? def)), ease: ease2 });
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 });
4405
4513
  }
4406
4514
  return keys;
4407
4515
  };
@@ -4412,6 +4520,10 @@ function importSpine(json, atlasText, opts = {}) {
4412
4520
  const seen = (frames) => {
4413
4521
  for (const f of frames) duration = Math.max(duration, f.time ?? 0);
4414
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
+ }
4415
4527
  for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
4416
4528
  const sb = spineBones[boneIndexByName.get(boneName)];
4417
4529
  if (!sb) {
@@ -4452,19 +4564,22 @@ function importSpine(json, atlasText, opts = {}) {
4452
4564
  if (kind === "attachment") {
4453
4565
  attachmentFrames.set(slotName, frames);
4454
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" }))));
4455
- } else if (kind === "rgba" || kind === "color") {
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";
4456
4570
  const rgbaOf = (f) => {
4457
- const c = hex8(f.color);
4458
- 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);
4459
4573
  };
4574
+ if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
4460
4575
  tracks.push(track("slot", id, "color", frames.map((f, i) => {
4461
4576
  const rgba = rgbaOf(f), next = frames[i + 1];
4462
- return { t: r4(f.time ?? 0), v: rgba.map(r4), ease: next ? easeOf(f.curve, 0, f.time ?? 0, next.time ?? 0, rgba[0], rgbaOf(next)[0]) : "linear" };
4463
- })));
4464
- } else if (kind === "rgb") {
4465
- tracks.push(track("slot", id, "color", frames.map((f) => {
4466
- const c = hex8((f.color ?? "ffffff") + "ff");
4467
- return { t: r4(f.time ?? 0), v: [0, 2, 4, 6].map((k) => r4(parseInt(c.slice(1 + k, 3 + k), 16) / 255)), ease: "linear" };
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
+ };
4468
4583
  })));
4469
4584
  } else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
4470
4585
  else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
@@ -4503,15 +4618,30 @@ function importSpine(json, atlasText, opts = {}) {
4503
4618
  warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
4504
4619
  continue;
4505
4620
  }
4506
- const length = att.weights ? 2 * att.weights.reduce((s, l) => s + l.length, 0) : att.vertices?.length ?? 0;
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;
4507
4625
  tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
4508
4626
  const full = new Array(length).fill(0);
4509
4627
  const off = f.offset ?? 0, vs = f.vertices ?? [];
4510
4628
  for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
4511
4629
  const next = frames[i + 1];
4512
- 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" };
4513
4631
  })));
4514
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
+ }
4515
4645
  const drawOrderFrames = a.drawOrder ?? a.draworder;
4516
4646
  if (Array.isArray(drawOrderFrames)) {
4517
4647
  seen(drawOrderFrames);
@@ -4534,20 +4664,25 @@ function importSpine(json, atlasText, opts = {}) {
4534
4664
  for (const [name, frames] of Object.entries(a.ik ?? {})) {
4535
4665
  seen(frames);
4536
4666
  const id = ikIds.get(name);
4667
+ if (!ik.some((c) => c.id === id)) continue;
4537
4668
  tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
4538
4669
  tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
4539
4670
  tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
4540
4671
  tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
4541
4672
  tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
4542
4673
  }
4543
- const mixTrack = (target, id, frames, prop, old, comp) => track(target, id, prop, frames.map((f, i) => ({
4544
- t: r4(f.time ?? 0),
4545
- v: r4(f[prop] ?? f[old] ?? 1),
4546
- ease: frames[i + 1] ? easeOf(f.curve, comp, f.time ?? 0, frames[i + 1].time ?? 0, f[prop] ?? f[old] ?? 1, frames[i + 1][prop] ?? frames[i + 1][old] ?? 1) : "linear"
4547
- })));
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
+ };
4548
4682
  for (const [name, frames] of Object.entries(a.transform ?? {})) {
4549
4683
  seen(frames);
4550
4684
  const id = tcIds.get(name);
4685
+ if (!transform.some((c) => c.id === id)) continue;
4551
4686
  const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
4552
4687
  for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
4553
4688
  }
@@ -4555,6 +4690,7 @@ function importSpine(json, atlasText, opts = {}) {
4555
4690
  const id = pcIds.get(name);
4556
4691
  for (const [kind, frames] of Object.entries(tl)) {
4557
4692
  seen(frames);
4693
+ if (!path.some((c) => c.id === id)) continue;
4558
4694
  if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
4559
4695
  else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
4560
4696
  tracks.push(mixTrack("path", id, frames, prop, old, comp));
@@ -4717,21 +4853,27 @@ async function importSpineProject(opts) {
4717
4853
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4718
4854
  const atlasText = await readFile7(atlasPath, "utf8");
4719
4855
  const { pages } = parseAtlas(atlasText);
4720
- if (pages.length !== 1) throw new Error(`expected one atlas page, got ${pages.length}`);
4721
- const pageStem = join8(dirname6(atlasPath), pages[0].name.replace(/\.[a-z0-9]+$/i, ""));
4722
- 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");
4723
4858
  const rigDir = dirname6(opts.rigPath);
4724
- let pngPath = imagePath;
4725
- if (extname(imagePath).toLowerCase() === ".ktx2") {
4726
- pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
4727
- if (!await exists(pngPath)) {
4728
- const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
4729
- await writeFile5(pngPath, png);
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
+ }
4730
4870
  }
4871
+ pagePaths.set(page2.name, pngPath);
4731
4872
  }
4732
- const pageImage = relative(rigDir, pngPath).split("\\").join("/");
4873
+ const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
4733
4874
  const name = opts.name ?? basename4(stem);
4734
- return { ...importSpine(json, atlasText, { name, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: pngPath };
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 };
4735
4877
  }
4736
4878
 
4737
4879
  // src/rig-templates.ts
@@ -5264,13 +5406,14 @@ function attachToBone(doc, animation, bone, target, t) {
5264
5406
  if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
5265
5407
  const [x, y] = apply(invert(to), from.tx, from.ty);
5266
5408
  const id = `attach_${bone}_${target}`;
5409
+ const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
5267
5410
  const offset = {
5268
5411
  x,
5269
5412
  y,
5270
- rotation: wrap2(rotationOf(from) - rotationOf(to)),
5413
+ rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
5271
5414
  scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
5272
5415
  scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
5273
- 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
5274
5417
  };
5275
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;
5276
5419
  let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
@@ -5533,10 +5676,11 @@ tool(
5533
5676
  var KeyIn = z3.object({
5534
5677
  t: z3.number().describe("seconds"),
5535
5678
  v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
5679
+ componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
5536
5680
  ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
5537
5681
  });
5538
5682
  var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
5539
- var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5683
+ var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5540
5684
  tool(
5541
5685
  "rig_create",
5542
5686
  "Create a new empty rig document with a root bone at bottom-center.",
@@ -5660,19 +5804,20 @@ tool(
5660
5804
  );
5661
5805
  tool(
5662
5806
  "rig_import_spine",
5663
- "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; a KTX2 page is transcoded to PNG next to rig.json. Then use rig_get / rig_render_preview / rig_check as with any rig.",
5807
+ "Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
5664
5808
  {
5665
5809
  path: Path,
5666
5810
  json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
5667
5811
  atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
5668
5812
  image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
5813
+ skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
5669
5814
  name: z3.string().optional().describe("default: the json file name"),
5670
5815
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5671
5816
  notes: z3.string().optional()
5672
5817
  },
5673
- async ({ path, json, atlas, image, name, margin, notes }) => {
5818
+ async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5674
5819
  try {
5675
- const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
5820
+ const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5676
5821
  const res = await create(path, parse(r.doc));
5677
5822
  if (r.warnings.length) res.text += `
5678
5823
  warnings: