@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.
package/dist/lib/cli.js CHANGED
@@ -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,7 +247,9 @@ 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", [
@@ -432,6 +436,10 @@ function validate(doc) {
432
436
  else if (a.slot !== s2.id) errs.push(`slot "${s2.id}": attachment "${s2.attachment}" belongs to slot "${a.slot}"`);
433
437
  }
434
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
+ }
435
443
  const vertexCount2 = (a) => {
436
444
  if (a.type === "region") return 4;
437
445
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -515,6 +523,7 @@ function validate(doc) {
515
523
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
516
524
  let prev = -1;
517
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`);
518
527
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
519
528
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
520
529
  if (t.target === "control") {
@@ -742,8 +751,8 @@ function sample(track, t, loop = false) {
742
751
  }
743
752
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
744
753
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
745
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
746
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
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));
747
756
  }
748
757
  return a.v;
749
758
  }
@@ -835,6 +844,13 @@ function boneWorldOf(bone, s2, parent) {
835
844
  if (!parent) return local;
836
845
  if (bone.inherit === "normal") return mul(parent, local);
837
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
+ }
838
854
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
839
855
  }
840
856
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -867,11 +883,11 @@ var localOf = (doc, pose, b) => {
867
883
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
868
884
  };
869
885
  };
870
- function updateSubtree(skel, root3) {
886
+ function updateSubtree(skel, root3, includeRoot = true) {
871
887
  const doc = skel.doc;
872
888
  const kids = /* @__PURE__ */ new Map();
873
889
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
874
- const stack = [root3];
890
+ const stack = includeRoot ? [root3] : (kids.get(root3) ?? []).map((b) => b.id);
875
891
  while (stack.length) {
876
892
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
877
893
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
@@ -1114,7 +1130,8 @@ function applyTransformConstraint(skel, c) {
1114
1130
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1115
1131
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
1116
1132
  const target = skel.bones.get(c.target), tw = target.world;
1117
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
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);
1118
1135
  for (const id of c.bones) {
1119
1136
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
1120
1137
  if (c.local) {
@@ -1276,6 +1293,7 @@ function applyPathConstraint(skel, c) {
1276
1293
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1277
1294
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1278
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 }));
1279
1297
  const spacesCount = tangents ? bones.length : bones.length + 1;
1280
1298
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1281
1299
  let sum = 0;
@@ -1310,9 +1328,10 @@ function applyPathConstraint(skel, c) {
1310
1328
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1311
1329
  }
1312
1330
  let boneX = samples[0].x, boneY = samples[0].y;
1331
+ const solvedWorlds = /* @__PURE__ */ new Map();
1313
1332
  for (let i = 0; i < bones.length; i++) {
1314
1333
  const { b, st } = bones[i];
1315
- let w = { ...st.world };
1334
+ let w = { ...inputWorlds[i] };
1316
1335
  w.tx += (boneX - w.tx) * mixX;
1317
1336
  w.ty += (boneY - w.ty) * mixY;
1318
1337
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1342,7 +1361,11 @@ function applyPathConstraint(skel, c) {
1342
1361
  const cos = Math.cos(r), sin = Math.sin(r);
1343
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 };
1344
1363
  }
1345
- setEffectiveWorld(skel, b, st, w);
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);
1346
1369
  }
1347
1370
  }
1348
1371
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1371,9 +1394,14 @@ function inheritedParentLinear(bone, parent) {
1371
1394
  case "onlyTranslation":
1372
1395
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1373
1396
  case "noRotationOrReflection": {
1374
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1397
+ const lenX = Math.hypot(parent.a, parent.b);
1375
1398
  const det = parent.a * parent.d - parent.b * parent.c;
1376
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
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 };
1377
1405
  }
1378
1406
  case "noScale":
1379
1407
  case "noScaleOrReflection": {
@@ -1399,7 +1427,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1399
1427
  st.scaleY = Math.hypot(lin.c, lin.d);
1400
1428
  st.shearX = 0;
1401
1429
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1402
- updateSubtree(skel, bone.id);
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);
1403
1440
  }
1404
1441
  function attachmentWorldVertices(skel, slotId, att) {
1405
1442
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -2158,7 +2195,7 @@ function removeSlot(doc, id) {
2158
2195
  return patchDrawOrder({
2159
2196
  ...doc,
2160
2197
  slots: doc.slots.filter((s2) => s2.id !== id).map((s2) => gone.has(s2.clip ?? "") ? { ...s2, clip: void 0 } : s2),
2161
- 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),
2162
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))) }))
2163
2200
  }, (o) => o.filter((s2) => s2 !== id));
2164
2201
  }
@@ -2554,7 +2591,7 @@ function rename2(doc, kind, id, to) {
2554
2591
  return {
2555
2592
  ...doc,
2556
2593
  slots: doc.slots.map((s2) => ({ ...s2, id: sub(s2.id) })),
2557
- 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) } } : {} })),
2558
2595
  path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
2559
2596
  };
2560
2597
  }
@@ -4167,6 +4204,9 @@ var Ids = class {
4167
4204
  has(name) {
4168
4205
  return this.map.has(name);
4169
4206
  }
4207
+ reserve(id) {
4208
+ this.used.add(id);
4209
+ }
4170
4210
  };
4171
4211
  var spineToDoc = (x, y, originX, originY) => [x + originX, originY - y];
4172
4212
  var r4 = (v) => Math.round(v * 1e6) / 1e6 + 0;
@@ -4175,6 +4215,7 @@ function importSpine(json, atlasText, opts = {}) {
4175
4215
  const warnings = [];
4176
4216
  const atlas = parseAtlas(atlasText);
4177
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`);
4178
4219
  const major = parseFloat(version);
4179
4220
  const bounds = { x: json.skeleton?.x ?? 0, y: json.skeleton?.y ?? 0, w: json.skeleton?.width ?? 0, h: json.skeleton?.height ?? 0 };
4180
4221
  const margin = opts.margin ?? 0;
@@ -4182,6 +4223,7 @@ function importSpine(json, atlasText, opts = {}) {
4182
4223
  const toDoc = (x, y) => spineToDoc(x, y, originX, originY);
4183
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_");
4184
4225
  const ROOT = "origin";
4226
+ boneIds.reserve(ROOT);
4185
4227
  const attachmentIdMap = /* @__PURE__ */ new Map();
4186
4228
  const attachmentId = (slot, name) => {
4187
4229
  const key = `${slot} ${name}`;
@@ -4206,13 +4248,17 @@ function importSpine(json, atlasText, opts = {}) {
4206
4248
  });
4207
4249
  }
4208
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)`);
4209
4255
  const frameSuffix = (seq, index) => {
4210
- const n = String((seq.start ?? 0) + index);
4256
+ const n = String((seq.start ?? 1) + index);
4211
4257
  return "0".repeat(Math.max(0, (seq.digits ?? 0) - n.length)) + n;
4212
4258
  };
4213
4259
  const sequences = /* @__PURE__ */ new Map();
4214
4260
  const skinnedName = /* @__PURE__ */ new Map();
4215
- for (const skin2 of skins) {
4261
+ for (const skin2 of lookupSkins) {
4216
4262
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4217
4263
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {}))
4218
4264
  for (const [name, a] of Object.entries(atts)) {
@@ -4221,7 +4267,10 @@ function importSpine(json, atlasText, opts = {}) {
4221
4267
  if (!skinnedName.has(key)) skinnedName.set(key, prefix + name + (seq ? frameSuffix(seq, seq.setupIndex ?? 0) : ""));
4222
4268
  }
4223
4269
  }
4224
- const attachmentRef = (slot, name) => attachmentId(slot, skinnedName.get(`${slot} ${name}`) ?? name);
4270
+ const attachmentRef = (slot, name) => {
4271
+ const resolved = skinnedName.get(`${slot} ${name}`);
4272
+ return resolved === void 0 ? null : attachmentId(slot, resolved);
4273
+ };
4225
4274
  const spineSlots = json.slots ?? [];
4226
4275
  const slotIndexByName = new Map(spineSlots.map((s2, i) => [s2.name, i]));
4227
4276
  const blendOf = (b) => b === "additive" ? "add" : b === "multiply" ? "multiply" : b === "screen" ? "screen" : "normal";
@@ -4229,11 +4278,12 @@ function importSpine(json, atlasText, opts = {}) {
4229
4278
  id: slotIds.get(s2.name),
4230
4279
  bone: boneIds.get(s2.bone),
4231
4280
  z: i,
4232
- ...s2.attachment ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
4281
+ ...s2.attachment && attachmentRef(s2.name, s2.attachment) ? { attachment: attachmentRef(s2.name, s2.attachment) } : {},
4233
4282
  blend: blendOf(s2.blend),
4234
4283
  color: hex8(s2.color),
4235
4284
  alpha: 1
4236
4285
  }));
4286
+ if (spineSlots.some((s2) => s2.dark)) warnings.push("Spine setup dark two-color tint is not supported");
4237
4287
  const pageImage = (page2) => typeof opts.pageImage === "function" ? opts.pageImage(page2) : opts.pageImage ?? page2;
4238
4288
  const assets = /* @__PURE__ */ new Map();
4239
4289
  const regionByName = new Map(atlas.regions.map((r) => [r.name, r]));
@@ -4259,6 +4309,7 @@ function importSpine(json, atlasText, opts = {}) {
4259
4309
  return id;
4260
4310
  };
4261
4311
  const attachments = [];
4312
+ const clipDeforms = /* @__PURE__ */ new Map();
4262
4313
  const readVertices = (a, vertexCount2) => {
4263
4314
  const v = a.vertices ?? [];
4264
4315
  if (v.length === vertexCount2 * 2) {
@@ -4279,6 +4330,22 @@ function importSpine(json, atlasText, opts = {}) {
4279
4330
  }
4280
4331
  return { weights };
4281
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();
4282
4349
  for (const skin2 of skins) {
4283
4350
  const prefix = skin2.name === "default" ? "" : `${skin2.name}_`;
4284
4351
  for (const [slotName, atts] of Object.entries(skin2.attachments ?? {})) {
@@ -4290,7 +4357,7 @@ function importSpine(json, atlasText, opts = {}) {
4290
4357
  for (const [name, a] of Object.entries(atts)) {
4291
4358
  if (a.sequence) {
4292
4359
  sequences.set(`${slotName} ${prefix}${name}`, a.sequence);
4293
- const base = a.path ?? name;
4360
+ const base = a.path ?? a.name ?? name;
4294
4361
  for (let i = 0; i < (a.sequence.count ?? 0); i++) {
4295
4362
  const suffix = frameSuffix(a.sequence, i), asset = assetFor(base + suffix);
4296
4363
  if (!asset) continue;
@@ -4313,7 +4380,7 @@ function importSpine(json, atlasText, opts = {}) {
4313
4380
  }
4314
4381
  const type = a.type ?? "region", id = attachmentId(slotName, prefix + name);
4315
4382
  if (type === "region") {
4316
- const asset = assetFor(a.path ?? name);
4383
+ const asset = assetFor(a.path ?? a.name ?? name);
4317
4384
  if (!asset) continue;
4318
4385
  attachments.push({
4319
4386
  id,
@@ -4329,21 +4396,48 @@ function importSpine(json, atlasText, opts = {}) {
4329
4396
  ...a.height ? { height: a.height } : {}
4330
4397
  });
4331
4398
  } else if (type === "mesh" || type === "linkedmesh") {
4332
- if (type === "linkedmesh") {
4333
- warnings.push(`linked mesh "${slotName}/${name}" skipped`);
4334
- continue;
4335
- }
4336
- 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);
4337
4402
  if (!asset) continue;
4338
- const n = (a.uvs?.length ?? 0) / 2;
4339
- 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 } : {} });
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
+ }
4340
4409
  } else if (type === "path") {
4341
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) } : {} } });
4342
4435
  } else warnings.push(`attachment "${slotName}/${name}" of type ${type} skipped`);
4343
4436
  }
4344
4437
  }
4345
4438
  }
4346
- const ik = (json.ik ?? []).map((c) => ({
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) => ({
4347
4441
  id: ikIds.get(c.name),
4348
4442
  order: c.order ?? 0,
4349
4443
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4356,7 +4450,7 @@ function importSpine(json, atlasText, opts = {}) {
4356
4450
  stretch: !!c.stretch,
4357
4451
  uniform: !!c.uniform
4358
4452
  }));
4359
- const transform = (json.transform ?? []).map((c) => ({
4453
+ const transform = (json.transform ?? []).filter((c) => activeConstraint("transform", c)).map((c) => ({
4360
4454
  id: tcIds.get(c.name),
4361
4455
  order: c.order ?? 0,
4362
4456
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4376,7 +4470,7 @@ function importSpine(json, atlasText, opts = {}) {
4376
4470
  mixScaleY: c.mixScaleY ?? c.mixScaleX ?? c.scaleMix ?? 1,
4377
4471
  mixShearY: c.mixShearY ?? c.shearMix ?? 1
4378
4472
  }));
4379
- const path = (json.path ?? []).map((c) => ({
4473
+ const path = (json.path ?? []).filter((c) => activeConstraint("path", c)).map((c) => ({
4380
4474
  id: pcIds.get(c.name),
4381
4475
  order: c.order ?? 0,
4382
4476
  bones: c.bones.map((b) => boneIds.get(b)),
@@ -4411,7 +4505,21 @@ function importSpine(json, atlasText, opts = {}) {
4411
4505
  for (let i = 0; i < frames.length; i++) {
4412
4506
  const f = frames[i], next = frames[i + 1], t = f.time ?? 0;
4413
4507
  const ease2 = next ? easeOf(f.curve, comp, t, next.time ?? 0, f[field] ?? def, next[field] ?? def) : "linear";
4414
- keys.push({ t: r4(t), v: r4(abs(f[field] ?? def)), ease: ease2 });
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 });
4415
4523
  }
4416
4524
  return keys;
4417
4525
  };
@@ -4422,6 +4530,10 @@ function importSpine(json, atlasText, opts = {}) {
4422
4530
  const seen = (frames) => {
4423
4531
  for (const f of frames) duration = Math.max(duration, f.time ?? 0);
4424
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
+ }
4425
4537
  for (const [boneName, tl] of Object.entries(a.bones ?? {})) {
4426
4538
  const sb = spineBones[boneIndexByName.get(boneName)];
4427
4539
  if (!sb) {
@@ -4462,19 +4574,22 @@ function importSpine(json, atlasText, opts = {}) {
4462
4574
  if (kind === "attachment") {
4463
4575
  attachmentFrames.set(slotName, frames);
4464
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" }))));
4465
- } else if (kind === "rgba" || kind === "color") {
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";
4466
4580
  const rgbaOf = (f) => {
4467
- const c = hex8(f.color);
4468
- 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);
4469
4583
  };
4584
+ if (kind.endsWith("2")) warnings.push(`${animName}/${slotName}: ${kind} light color imported; dark two-color tint is not supported`);
4470
4585
  tracks.push(track("slot", id, "color", frames.map((f, i) => {
4471
4586
  const rgba = rgbaOf(f), next = frames[i + 1];
4472
- 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" };
4473
- })));
4474
- } else if (kind === "rgb") {
4475
- tracks.push(track("slot", id, "color", frames.map((f) => {
4476
- const c = hex8((f.color ?? "ffffff") + "ff");
4477
- 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" };
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
+ };
4478
4593
  })));
4479
4594
  } else if (kind === "alpha") tracks.push(track("slot", id, "alpha", numeric(frames, "value", 1, (v) => v, 0)));
4480
4595
  else warnings.push(`${animName}/${slotName}: ${kind} timeline skipped`);
@@ -4513,15 +4628,30 @@ function importSpine(json, atlasText, opts = {}) {
4513
4628
  warnings.push(`${animName}: deform of unknown attachment ${slotName}/${attName}`);
4514
4629
  continue;
4515
4630
  }
4516
- const length = att.weights ? 2 * att.weights.reduce((s2, l) => s2 + l.length, 0) : att.vertices?.length ?? 0;
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;
4517
4635
  tracks.push(track("deform", id, "vertices", frames.map((f, i) => {
4518
4636
  const full = new Array(length).fill(0);
4519
4637
  const off = f.offset ?? 0, vs = f.vertices ?? [];
4520
4638
  for (let k = 0; k < vs.length; k++) full[off + k] = (k + off) % 2 === 1 ? -vs[k] : vs[k];
4521
4639
  const next = frames[i + 1];
4522
- 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" };
4523
4641
  })));
4524
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
+ }
4525
4655
  const drawOrderFrames = a.drawOrder ?? a.draworder;
4526
4656
  if (Array.isArray(drawOrderFrames)) {
4527
4657
  seen(drawOrderFrames);
@@ -4544,20 +4674,25 @@ function importSpine(json, atlasText, opts = {}) {
4544
4674
  for (const [name, frames] of Object.entries(a.ik ?? {})) {
4545
4675
  seen(frames);
4546
4676
  const id = ikIds.get(name);
4677
+ if (!ik.some((c) => c.id === id)) continue;
4547
4678
  tracks.push(track("ik", id, "mix", numeric(frames, "mix", 1, (v) => v, 0)));
4548
4679
  tracks.push(track("ik", id, "softness", numeric(frames, "softness", 0, (v) => v, 1)));
4549
4680
  tracks.push(track("ik", id, "bendPositive", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.bendPositive ?? true ? 0 : 1, ease: "step" }))));
4550
4681
  tracks.push(track("ik", id, "compress", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.compress ? 1 : 0, ease: "step" }))));
4551
4682
  tracks.push(track("ik", id, "stretch", frames.map((f) => ({ t: r4(f.time ?? 0), v: f.stretch ? 1 : 0, ease: "step" }))));
4552
4683
  }
4553
- const mixTrack = (target, id, frames, prop, old, comp) => track(target, id, prop, frames.map((f, i) => ({
4554
- t: r4(f.time ?? 0),
4555
- v: r4(f[prop] ?? f[old] ?? 1),
4556
- 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"
4557
- })));
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
+ };
4558
4692
  for (const [name, frames] of Object.entries(a.transform ?? {})) {
4559
4693
  seen(frames);
4560
4694
  const id = tcIds.get(name);
4695
+ if (!transform.some((c) => c.id === id)) continue;
4561
4696
  const props = [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2], ["mixScaleX", "scaleMix", 3], ["mixScaleY", "scaleMix", 4], ["mixShearY", "shearMix", 5]];
4562
4697
  for (const [prop, old, comp] of props) tracks.push(mixTrack("transform", id, frames, prop, old, comp));
4563
4698
  }
@@ -4565,6 +4700,7 @@ function importSpine(json, atlasText, opts = {}) {
4565
4700
  const id = pcIds.get(name);
4566
4701
  for (const [kind, frames] of Object.entries(tl)) {
4567
4702
  seen(frames);
4703
+ if (!path.some((c) => c.id === id)) continue;
4568
4704
  if (kind === "position" || kind === "spacing") tracks.push(track("path", id, kind, numeric(frames, "value", 0, (v) => v, 0)));
4569
4705
  else if (kind === "mix") for (const [prop, old, comp] of [["mixRotate", "rotateMix", 0], ["mixX", "translateMix", 1], ["mixY", "translateMix", 2]])
4570
4706
  tracks.push(mixTrack("path", id, frames, prop, old, comp));
@@ -4727,21 +4863,27 @@ async function importSpineProject(opts) {
4727
4863
  const atlasPath = opts.atlas ?? await firstExisting([stem + ".atlas", stem + ".atlas.txt"], "atlas");
4728
4864
  const atlasText = await readFile7(atlasPath, "utf8");
4729
4865
  const { pages } = parseAtlas(atlasText);
4730
- if (pages.length !== 1) throw new Error(`expected one atlas page, got ${pages.length}`);
4731
- const pageStem = join8(dirname6(atlasPath), pages[0].name.replace(/\.[a-z0-9]+$/i, ""));
4732
- 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");
4733
4868
  const rigDir = dirname6(opts.rigPath);
4734
- let pngPath = imagePath;
4735
- if (extname(imagePath).toLowerCase() === ".ktx2") {
4736
- pngPath = join8(rigDir, basename4(imagePath).replace(/\.ktx2$/i, ".png"));
4737
- if (!await exists(pngPath)) {
4738
- const { png } = await ktx2ToPng(new Uint8Array(await readFile7(imagePath)));
4739
- await writeFile5(pngPath, png);
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
+ }
4740
4880
  }
4881
+ pagePaths.set(page2.name, pngPath);
4741
4882
  }
4742
- const pageImage = relative(rigDir, pngPath).split("\\").join("/");
4883
+ const pageImage = (page2) => relative(rigDir, pagePaths.get(page2)).split("\\").join("/");
4743
4884
  const name = opts.name ?? basename4(stem);
4744
- return { ...importSpine(json, atlasText, { name, pageImage, margin: opts.margin, notes: opts.notes }), imagePath: pngPath };
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 };
4745
4887
  }
4746
4888
 
4747
4889
  // src/rig-templates.ts
@@ -5274,13 +5416,14 @@ function attachToBone(doc, animation, bone, target, t) {
5274
5416
  if (Math.abs(to.a * to.d - to.b * to.c) < 1e-10) throw new Error("cannot attach to a singular target");
5275
5417
  const [x, y] = apply(invert(to), from.tx, from.ty);
5276
5418
  const id = `attach_${bone}_${target}`;
5419
+ const handedness = to.a * to.d - to.b * to.c < 0 ? -1 : 1;
5277
5420
  const offset = {
5278
5421
  x,
5279
5422
  y,
5280
- rotation: wrap2(rotationOf(from) - rotationOf(to)),
5423
+ rotation: wrap2(rotationOf(from) - rotationOf(to)) * handedness,
5281
5424
  scaleX: Math.hypot(from.a, from.b) - Math.hypot(to.a, to.b),
5282
5425
  scaleY: Math.hypot(from.c, from.d) - Math.hypot(to.c, to.d),
5283
- 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
5284
5427
  };
5285
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;
5286
5429
  let next = setConstraint(doc, "transform", id, { bones: [bone], target, order, ...offset, mixX: 0, mixY: 0, mixRotate: 0, mixScaleX: 0, mixScaleY: 0, mixShearY: 0 });
@@ -5543,10 +5686,11 @@ tool(
5543
5686
  var KeyIn = z3.object({
5544
5687
  t: z3.number().describe("seconds"),
5545
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"),
5546
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")
5547
5691
  });
5548
5692
  var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
5549
- 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].";
5550
5694
  tool(
5551
5695
  "rig_create",
5552
5696
  "Create a new empty rig document with a root bone at bottom-center.",
@@ -5670,19 +5814,20 @@ tool(
5670
5814
  );
5671
5815
  tool(
5672
5816
  "rig_import_spine",
5673
- "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.",
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.",
5674
5818
  {
5675
5819
  path: Path,
5676
5820
  json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
5677
5821
  atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
5678
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"),
5679
5824
  name: z3.string().optional().describe("default: the json file name"),
5680
5825
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5681
5826
  notes: z3.string().optional()
5682
5827
  },
5683
- async ({ path, json, atlas, image, name, margin, notes }) => {
5828
+ async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5684
5829
  try {
5685
- 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 });
5686
5831
  const res = await create(path, parse(r.doc));
5687
5832
  if (r.warnings.length) res.text += `
5688
5833
  warnings: