@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.
@@ -173,7 +173,9 @@ var PathAttachment = z.object({
173
173
  /** curve lengths for constant-speed positioning */
174
174
  lengths: z.array(z.number()).default([]),
175
175
  closed: z.boolean().default(false),
176
- constantSpeed: z.boolean().default(true)
176
+ constantSpeed: z.boolean().default(true),
177
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
178
+ clipping: z.object({ end: Id.optional() }).optional()
177
179
  });
178
180
  var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
179
181
  var IkConstraint = z.object({
@@ -242,7 +244,9 @@ var Key = z.object({
242
244
  t: z.number().nonnegative(),
243
245
  v: KeyValue,
244
246
  /** easing INTO the next key */
245
- ease: Ease.default("ease_in_out")
247
+ ease: Ease.default("ease_in_out"),
248
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
249
+ componentEase: z.array(Ease).min(1).optional()
246
250
  });
247
251
  var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
248
252
  var Track = z.discriminatedUnion("target", [
@@ -429,6 +433,10 @@ function validate(doc) {
429
433
  else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
430
434
  }
431
435
  }
436
+ for (const a of doc.attachments) if (a.type === "path" && a.clipping) {
437
+ if (!a.closed) errs.push(`attachment "${a.id}": clipping path must be closed`);
438
+ if (a.clipping.end && !slots.has(a.clipping.end)) errs.push(`attachment "${a.id}": unknown clipping end slot "${a.clipping.end}"`);
439
+ }
432
440
  const vertexCount2 = (a) => {
433
441
  if (a.type === "region") return 4;
434
442
  if (a.type === "mesh") return a.uvs.length / 2;
@@ -512,6 +520,7 @@ function validate(doc) {
512
520
  const att = t.target === "deform" ? attachments.get(t.id) : void 0;
513
521
  let prev = -1;
514
522
  for (const k of t.keys) {
523
+ if (k.componentEase && (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number") || k.componentEase.length !== k.v.length)) errs.push(`animation "${a.id}" ${tid}: componentEase must match the numeric-array value length`);
515
524
  if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
516
525
  if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
517
526
  if (t.target === "control") {
@@ -739,8 +748,8 @@ function sample(track, t, loop = false) {
739
748
  }
740
749
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
741
750
  if (track.interpolation === "pchip") return a.v.map((_, j2) => smoothValue(track, i, t, j2, loop));
742
- const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
743
- return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
751
+ const phase = (t - a.t) / (b.t - a.t), av = a.v, bv = b.v;
752
+ return av.map((v, k) => v + ((bv[k] ?? v) - v) * ease(a.componentEase?.[k] ?? a.ease)(phase));
744
753
  }
745
754
  return a.v;
746
755
  }
@@ -832,6 +841,13 @@ function boneWorldOf(bone, s, parent) {
832
841
  if (!parent) return local;
833
842
  if (bone.inherit === "normal") return mul(parent, local);
834
843
  const [tx, ty] = apply(parent, s.x, s.y);
844
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
845
+ const r = s.rotation * DEG2, x = parent.a * Math.cos(r) + parent.c * Math.sin(r), y = parent.b * Math.cos(r) + parent.d * Math.sin(r);
846
+ const length = Math.hypot(x, y), a = length > 1e-5 ? x / length : x, b = length > 1e-5 ? y / length : y;
847
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
848
+ const basis3 = { a, b, c: -b * sign, d: a * sign, tx: 0, ty: 0 };
849
+ return { ...mul(basis3, affineOf(0, 0, 0, s.scaleX, s.scaleY, s.shearX, s.shearY)), tx, ty };
850
+ }
835
851
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
836
852
  }
837
853
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -864,11 +880,11 @@ var localOf = (doc, pose, b) => {
864
880
  shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
865
881
  };
866
882
  };
867
- function updateSubtree(skel, root2) {
883
+ function updateSubtree(skel, root2, includeRoot = true) {
868
884
  const doc = skel.doc;
869
885
  const kids = /* @__PURE__ */ new Map();
870
886
  for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
871
- const stack = [root2];
887
+ const stack = includeRoot ? [root2] : (kids.get(root2) ?? []).map((b) => b.id);
872
888
  while (stack.length) {
873
889
  const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
874
890
  st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
@@ -1111,7 +1127,8 @@ function applyTransformConstraint(skel, c) {
1111
1127
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
1112
1128
  c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
1113
1129
  const target = skel.bones.get(c.target), tw = target.world;
1114
- const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
1130
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
1131
+ const offsetRotation = c.rotation * DEG2 * (c.local ? 1 : handedness), offsetShearY = c.shearY * DEG2 * (c.local ? 1 : handedness);
1115
1132
  for (const id of c.bones) {
1116
1133
  const b = boneOf(skel.doc, id), st = skel.bones.get(id);
1117
1134
  if (c.local) {
@@ -1273,6 +1290,7 @@ function applyPathConstraint(skel, c) {
1273
1290
  const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
1274
1291
  const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
1275
1292
  const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
1293
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
1276
1294
  const spacesCount = tangents ? bones.length : bones.length + 1;
1277
1295
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
1278
1296
  let sum = 0;
@@ -1307,9 +1325,10 @@ function applyPathConstraint(skel, c) {
1307
1325
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
1308
1326
  }
1309
1327
  let boneX = samples[0].x, boneY = samples[0].y;
1328
+ const solvedWorlds = /* @__PURE__ */ new Map();
1310
1329
  for (let i = 0; i < bones.length; i++) {
1311
1330
  const { b, st } = bones[i];
1312
- let w = { ...st.world };
1331
+ let w = { ...inputWorlds[i] };
1313
1332
  w.tx += (boneX - w.tx) * mixX;
1314
1333
  w.ty += (boneY - w.ty) * mixY;
1315
1334
  const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -1339,7 +1358,11 @@ function applyPathConstraint(skel, c) {
1339
1358
  const cos = Math.cos(r), sin = Math.sin(r);
1340
1359
  w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
1341
1360
  }
1342
- setEffectiveWorld(skel, b, st, w);
1361
+ solvedWorlds.set(b.id, w);
1362
+ }
1363
+ for (const b of boneOrder(skel.doc)) {
1364
+ const world = solvedWorlds.get(b.id);
1365
+ if (world) setEffectiveWorld(skel, b, skel.bones.get(b.id), world);
1343
1366
  }
1344
1367
  }
1345
1368
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -1368,9 +1391,14 @@ function inheritedParentLinear(bone, parent) {
1368
1391
  case "onlyTranslation":
1369
1392
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
1370
1393
  case "noRotationOrReflection": {
1371
- const lenX = Math.hypot(parent.a, parent.b) || 1;
1394
+ const lenX = Math.hypot(parent.a, parent.b);
1372
1395
  const det = parent.a * parent.d - parent.b * parent.c;
1373
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
1396
+ if (lenX * lenX <= 1e-4) {
1397
+ const lenY = Math.hypot(parent.c, parent.d), ux2 = lenY ? parent.c / lenY : 0, uy2 = lenY ? parent.d / lenY : 0;
1398
+ return { a: lenY * ux2 * ux2, b: lenY * ux2 * uy2, c: lenY * ux2 * uy2, d: lenY * uy2 * uy2, tx: 0, ty: 0 };
1399
+ }
1400
+ const ux = parent.a / lenX, uy = parent.b / lenX, sy = Math.abs(det) / lenX;
1401
+ return { a: lenX * ux * ux + sy * uy * uy, b: (lenX - sy) * ux * uy, c: (lenX - sy) * ux * uy, d: lenX * uy * uy + sy * ux * ux, tx: 0, ty: 0 };
1374
1402
  }
1375
1403
  case "noScale":
1376
1404
  case "noScaleOrReflection": {
@@ -1396,7 +1424,16 @@ function setEffectiveWorld(skel, bone, st, world) {
1396
1424
  st.scaleY = Math.hypot(lin.c, lin.d);
1397
1425
  st.shearX = 0;
1398
1426
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
1399
- updateSubtree(skel, bone.id);
1427
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
1428
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
1429
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
1430
+ st.scaleX = Math.hypot(world.a, world.b);
1431
+ st.scaleY = Math.hypot(world.c, world.d);
1432
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
1433
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
1434
+ }
1435
+ st.world = world;
1436
+ updateSubtree(skel, bone.id, false);
1400
1437
  }
1401
1438
  function attachmentWorldVertices(skel, slotId, att) {
1402
1439
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -2155,7 +2192,7 @@ function removeSlot(doc, id) {
2155
2192
  return patchDrawOrder({
2156
2193
  ...doc,
2157
2194
  slots: doc.slots.filter((s) => s.id !== id).map((s) => gone.has(s.clip ?? "") ? { ...s, clip: void 0 } : s),
2158
- attachments: doc.attachments.filter((a) => a.slot !== id),
2195
+ attachments: doc.attachments.filter((a) => a.slot !== id).map((a) => a.type === "path" && a.clipping?.end === id ? { ...a, clipping: {} } : a),
2159
2196
  animations: doc.animations.map((a) => ({ ...a, tracks: a.tracks.filter((t) => !(t.target === "slot" && t.id === id || t.target === "deform" && gone.has(t.id))) }))
2160
2197
  }, (o) => o.filter((s) => s !== id));
2161
2198
  }
@@ -2551,7 +2588,7 @@ function rename2(doc, kind, id, to) {
2551
2588
  return {
2552
2589
  ...doc,
2553
2590
  slots: doc.slots.map((s) => ({ ...s, id: sub(s.id) })),
2554
- attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot) })),
2591
+ attachments: doc.attachments.map((a) => ({ ...a, slot: sub(a.slot), ...a.type === "path" && a.clipping?.end ? { clipping: { end: sub(a.clipping.end) } } : {} })),
2555
2592
  path: doc.path.map((c) => ({ ...c, target: sub(c.target) }))
2556
2593
  };
2557
2594
  }
@@ -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 });
@@ -5532,10 +5675,11 @@ tool(
5532
5675
  var KeyIn = z3.object({
5533
5676
  t: z3.number().describe("seconds"),
5534
5677
  v: z3.union([z3.number(), z3.string(), z3.null(), z3.array(z3.number()), z3.array(z3.string())]),
5678
+ componentEase: z3.array(Ease).min(1).optional().describe("optional independent easing per numeric-array component"),
5535
5679
  ease: Ease.optional().describe("into the next key: linear|step|ease_in|ease_out|ease_in_out|back_out|elastic_out|{bezier:[x1,y1,x2,y2]}; default ease_in_out; minimum_jerk for a rest-to-rest segment")
5536
5680
  });
5537
5681
  var roundKey = (k) => Array.isArray(k.v) && k.v.every((x) => typeof x === "number") ? { ...k, v: k.v.map(round6) } : k;
5538
- var KEY_DOC = "Keys are {t, v, ease?}. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5682
+ var KEY_DOC = "Keys are {t, v, ease?, componentEase?}; componentEase selects independent easing for numeric-array components. Bone values are ABSOLUTE local-transform values, not offsets (rest x=4 \u2192 key v=0 moves it 4px); deform values are relative (0 = rest). bone: x|y|rotation|scaleX|scaleY|shearX|shearY; slot: alpha|attachment(asset id or null)|z; deform (3\xD73 mesh on the slot's image): bulge|bend|sway|squash (fractions of the image size, |v|\u22640.9/1) or p<row><col>x|y (px, rows/cols 0..2 top-left first). Some values are arrays: deform `vertices` keys are number arrays (2\xB7n offsets, or 2\xB7\u03A3influences for weighted meshes); drawOrder keys are slot-id arrays; slot color keys are [r,g,b,a].";
5539
5683
  tool(
5540
5684
  "rig_create",
5541
5685
  "Create a new empty rig document with a root bone at bottom-center.",
@@ -5659,19 +5803,20 @@ tool(
5659
5803
  );
5660
5804
  tool(
5661
5805
  "rig_import_spine",
5662
- "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.",
5806
+ "Import a Spine project (skeleton .json 3.8/4.x + .atlas + page image) as a new rig: bones, slots, region/mesh/path attachments with weights, IK / transform / path constraints and every animation (deform, draw order, colors), converted to this document's y-down space. The atlas may be plain or base64 text; named pages resolve beside the atlas, and KTX2 pages transcode to PNG next to rig.json. Read warnings for unsupported physics and dark tint. Then use rig_get / rig_render_preview / rig_check as with any rig.",
5663
5807
  {
5664
5808
  path: Path,
5665
5809
  json: z3.string().describe("Spine skeleton .json, absolute or relative to rig.json dir"),
5666
5810
  atlas: z3.string().optional().describe("atlas file; default: <json>.atlas or <json>.atlas.txt"),
5667
5811
  image: z3.string().optional().describe("atlas page image (.png or .ktx2); default: the page named in the atlas, next to it"),
5812
+ skin: z3.string().optional().describe("skin to flatten; default: default skin, or first skin if no default exists; no runtime skin switching"),
5668
5813
  name: z3.string().optional().describe("default: the json file name"),
5669
5814
  margin: z3.number().optional().describe("canvas margin around the skeleton bounds, px"),
5670
5815
  notes: z3.string().optional()
5671
5816
  },
5672
- async ({ path, json, atlas, image, name, margin, notes }) => {
5817
+ async ({ path, json, atlas, image, name, skin: skin2, margin, notes }) => {
5673
5818
  try {
5674
- const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, margin, notes });
5819
+ const r = await importSpineProject({ json: resolveFrom(path, json), atlas: atlas && resolveFrom(path, atlas), image: image && resolveFrom(path, image), rigPath: path, name, skin: skin2, margin, notes });
5675
5820
  const res = await create(path, parse(r.doc));
5676
5821
  if (r.warnings.length) res.text += `
5677
5822
  warnings: