@energy8platform/golem 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49524,7 +49524,9 @@ ${parts.join("\n")}
49524
49524
  /** curve lengths for constant-speed positioning */
49525
49525
  lengths: external_exports.array(external_exports.number()).default([]),
49526
49526
  closed: external_exports.boolean().default(false),
49527
- constantSpeed: external_exports.boolean().default(true)
49527
+ constantSpeed: external_exports.boolean().default(true),
49528
+ /** Spine-style active clipping span in the posed draw order; end slot is inclusive. */
49529
+ clipping: external_exports.object({ end: Id.optional() }).optional()
49528
49530
  });
49529
49531
  var Attachment = external_exports.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
49530
49532
  var IkConstraint = external_exports.object({
@@ -49588,10 +49590,13 @@ ${parts.join("\n")}
49588
49590
  t: external_exports.number().nonnegative(),
49589
49591
  v: KeyValue,
49590
49592
  /** easing INTO the next key */
49591
- ease: Ease.default("ease_in_out")
49593
+ ease: Ease.default("ease_in_out"),
49594
+ /** Optional independent easing for numeric-array components (for example Spine RGBA curves). */
49595
+ componentEase: external_exports.array(Ease).min(1).optional()
49592
49596
  });
49593
49597
  var Interpolation = { interpolation: external_exports.enum(["segment", "pchip"]).optional() };
49594
49598
  var Track = external_exports.discriminatedUnion("target", [
49599
+ external_exports.object({ target: external_exports.literal("control"), id: Id, prop: external_exports.literal("value"), ...Interpolation, keys: external_exports.array(Key).min(1) }),
49595
49600
  external_exports.object({ target: external_exports.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
49596
49601
  external_exports.object({ target: external_exports.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
49597
49602
  external_exports.object({ target: external_exports.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: external_exports.array(Key).min(1) }),
@@ -49614,6 +49619,25 @@ ${parts.join("\n")}
49614
49619
  /** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
49615
49620
  preset: external_exports.object({ name: external_exports.string(), params: external_exports.record(external_exports.unknown()) }).optional()
49616
49621
  });
49622
+ var RigControl = external_exports.object({
49623
+ id: Id,
49624
+ label: external_exports.string().optional(),
49625
+ min: external_exports.number().finite(),
49626
+ max: external_exports.number().finite(),
49627
+ default: external_exports.number().finite()
49628
+ }).strict();
49629
+ var ControlValues = external_exports.array(external_exports.union([external_exports.number().finite(), external_exports.array(external_exports.number().finite()).min(1)])).min(2);
49630
+ var ControlChannel = external_exports.discriminatedUnion("target", [
49631
+ external_exports.object({ target: external_exports.literal("bone"), id: Id, prop: BoneProp, values: ControlValues }).strict(),
49632
+ external_exports.object({ target: external_exports.literal("slot"), id: Id, prop: external_exports.literal("alpha"), values: ControlValues }).strict(),
49633
+ external_exports.object({ target: external_exports.literal("deform"), id: Id, prop: external_exports.literal("vertices"), values: ControlValues }).strict()
49634
+ ]);
49635
+ var ControlBinding = external_exports.object({
49636
+ id: Id,
49637
+ axes: external_exports.array(external_exports.object({ control: Id, keys: external_exports.array(external_exports.number().finite()).min(2).max(32) }).strict()).min(1).max(2),
49638
+ /** Flattened grid, first axis varies fastest. Linear / bilinear interpolation. */
49639
+ channels: external_exports.array(ControlChannel).min(1)
49640
+ }).strict();
49617
49641
  var Reference = external_exports.object({
49618
49642
  src: external_exports.string(),
49619
49643
  opacity: external_exports.number().min(0).max(1).default(0.5),
@@ -49639,7 +49663,9 @@ ${parts.join("\n")}
49639
49663
  ik: external_exports.array(IkConstraint).default([]),
49640
49664
  transform: external_exports.array(TransformConstraint).default([]),
49641
49665
  path: external_exports.array(PathConstraint).default([]),
49642
- animations: external_exports.array(Animation)
49666
+ animations: external_exports.array(Animation),
49667
+ controls: external_exports.array(RigControl).optional(),
49668
+ controlBindings: external_exports.array(ControlBinding).optional()
49643
49669
  });
49644
49670
  var RigDocument = external_exports.preprocess((input) => {
49645
49671
  const v2 = input?.version;
@@ -49714,6 +49740,8 @@ ${parts.join("\n")}
49714
49740
  dup(doc.ik, "ik");
49715
49741
  dup(doc.transform, "transform");
49716
49742
  dup(doc.path, "path");
49743
+ dup(doc.controls ?? [], "control");
49744
+ dup(doc.controlBindings ?? [], "control binding");
49717
49745
  const bones = new Set(doc.bones.map((b2) => b2.id));
49718
49746
  const assets = new Map(doc.assets.map((a2) => [a2.id, a2]));
49719
49747
  const slots = new Set(doc.slots.map((s2) => s2.id));
@@ -49751,6 +49779,10 @@ ${parts.join("\n")}
49751
49779
  else if (a2.slot !== s2.id) errs.push(`slot "${s2.id}": attachment "${s2.attachment}" belongs to slot "${a2.slot}"`);
49752
49780
  }
49753
49781
  }
49782
+ for (const a2 of doc.attachments) if (a2.type === "path" && a2.clipping) {
49783
+ if (!a2.closed) errs.push(`attachment "${a2.id}": clipping path must be closed`);
49784
+ if (a2.clipping.end && !slots.has(a2.clipping.end)) errs.push(`attachment "${a2.id}": unknown clipping end slot "${a2.clipping.end}"`);
49785
+ }
49754
49786
  const vertexCount = (a2) => {
49755
49787
  if (a2.type === "region") return 4;
49756
49788
  if (a2.type === "mesh") return a2.uvs.length / 2;
@@ -49788,20 +49820,59 @@ ${parts.join("\n")}
49788
49820
  for (const b2 of c2.bones) if (!bones.has(b2)) errs.push(`path "${c2.id}": unknown bone "${b2}"`);
49789
49821
  if (!slots.has(c2.target)) errs.push(`path "${c2.id}": unknown target slot "${c2.target}"`);
49790
49822
  }
49823
+ const controls = new Map((doc.controls ?? []).map((c2) => [c2.id, c2]));
49824
+ const driven = /* @__PURE__ */ new Set();
49825
+ for (const c2 of controls.values()) if (!(c2.min < c2.max && c2.default >= c2.min && c2.default <= c2.max))
49826
+ errs.push(`control "${c2.id}": require min < max and default within range`);
49827
+ for (const b2 of doc.controlBindings ?? []) {
49828
+ const prefix = `control binding "${b2.id}"`;
49829
+ if (new Set(b2.axes.map((a2) => a2.control)).size !== b2.axes.length) errs.push(`${prefix}: axes must use different controls`);
49830
+ for (const axis of b2.axes) {
49831
+ const c2 = controls.get(axis.control);
49832
+ if (!c2) errs.push(`${prefix}: unknown control "${axis.control}"`);
49833
+ else if (axis.keys[0] !== c2.min || axis.keys.at(-1) !== c2.max) errs.push(`${prefix}: keys must span the control's full range`);
49834
+ if (axis.keys.some((v2, i2) => i2 > 0 && v2 <= axis.keys[i2 - 1])) errs.push(`${prefix}: keys must be strictly increasing`);
49835
+ }
49836
+ const count2 = b2.axes.reduce((n2, a2) => n2 * a2.keys.length, 1);
49837
+ for (const ch of b2.channels) {
49838
+ const key = `${ch.target}.${ch.id}.${ch.prop}`;
49839
+ if (driven.has(key)) errs.push(`${prefix}: channel ${key} already owned by a binding`);
49840
+ driven.add(key);
49841
+ if (ch.values.length !== count2) errs.push(`${prefix}: ${key} needs ${count2} grid values`);
49842
+ if (ch.target === "deform") {
49843
+ const att = attachments.get(ch.id);
49844
+ if (!att || att.type === "region") errs.push(`${prefix}: vertices need a mesh or path attachment`);
49845
+ else {
49846
+ const n2 = att.weights ? att.weights.reduce((s2, a2) => s2 + a2.length * 2, 0) : att.vertices.length;
49847
+ if (ch.values.some((v2) => !Array.isArray(v2) || v2.length !== n2)) errs.push(`${prefix}: ${key} needs arrays of ${n2} offsets`);
49848
+ }
49849
+ } else {
49850
+ if (!(ch.target === "bone" ? bones : slots).has(ch.id)) errs.push(`${prefix}: unknown ${ch.target} "${ch.id}"`);
49851
+ if (ch.values.some((v2) => typeof v2 !== "number")) errs.push(`${prefix}: ${key} needs numeric values`);
49852
+ if (ch.target === "slot" && ch.values.some((v2) => typeof v2 === "number" && (v2 < 0 || v2 > 1))) errs.push(`${prefix}: alpha must be in 0..1`);
49853
+ }
49854
+ }
49855
+ }
49791
49856
  for (const a2 of doc.animations) {
49792
49857
  const tracks = /* @__PURE__ */ new Set();
49793
49858
  for (const t2 of a2.tracks) {
49794
49859
  const tid = trackId(t2);
49795
49860
  if (tracks.has(tid)) errs.push(`animation "${a2.id}": duplicate track ${tid}`);
49796
49861
  tracks.add(tid);
49797
- const known = t2.target === "bone" ? bones.has(t2.id) : t2.target === "slot" ? slots.has(t2.id) : t2.target === "deform" ? attachments.has(t2.id) : t2.target === "ik" ? iks.has(t2.id) : t2.target === "transform" ? transforms.has(t2.id) : t2.target === "path" ? paths.has(t2.id) : true;
49862
+ if (driven.has(tid)) errs.push(`animation "${a2.id}": ${tid} is controlled by a binding; animate its control instead`);
49863
+ const known = t2.target === "bone" ? bones.has(t2.id) : t2.target === "slot" ? slots.has(t2.id) : t2.target === "deform" ? attachments.has(t2.id) : t2.target === "ik" ? iks.has(t2.id) : t2.target === "transform" ? transforms.has(t2.id) : t2.target === "path" ? paths.has(t2.id) : t2.target === "control" ? controls.has(t2.id) : true;
49798
49864
  if (!known) errs.push(`animation "${a2.id}": unknown ${t2.target} "${t2.id}"`);
49799
49865
  const bound = t2.target === "deform" ? DEFORM_BOUNDS[t2.prop] : void 0;
49800
49866
  const att = t2.target === "deform" ? attachments.get(t2.id) : void 0;
49801
49867
  let prev = -1;
49802
49868
  for (const k2 of t2.keys) {
49869
+ if (k2.componentEase && (!Array.isArray(k2.v) || !k2.v.every((v2) => typeof v2 === "number") || k2.componentEase.length !== k2.v.length)) errs.push(`animation "${a2.id}" ${tid}: componentEase must match the numeric-array value length`);
49803
49870
  if (k2.t <= prev && prev >= 0) errs.push(`animation "${a2.id}" ${tid}: keys not strictly increasing`);
49804
49871
  if (k2.t > a2.duration) errs.push(`animation "${a2.id}" ${tid}: key at ${k2.t}s exceeds duration`);
49872
+ if (t2.target === "control") {
49873
+ const c2 = controls.get(t2.id);
49874
+ if (typeof k2.v !== "number" || !Number.isFinite(k2.v) || c2 && (k2.v < c2.min || k2.v > c2.max)) errs.push(`animation "${a2.id}" ${tid}: control key outside its finite range`);
49875
+ }
49805
49876
  if (t2.target === "slot" && t2.prop === "attachment") {
49806
49877
  if (k2.v !== null && typeof k2.v !== "string") errs.push(`animation "${a2.id}" ${tid}: attachment key must be an attachment id or null`);
49807
49878
  else if (typeof k2.v === "string") {
@@ -49840,6 +49911,52 @@ ${parts.join("\n")}
49840
49911
  return doc;
49841
49912
  }
49842
49913
 
49914
+ // src/rig-controls.ts
49915
+ function controlPose(doc, values) {
49916
+ const out2 = /* @__PURE__ */ new Map();
49917
+ for (const [id, value] of Object.entries(values)) {
49918
+ const c2 = doc.controls?.find((c3) => c3.id === id);
49919
+ if (!c2) throw new Error(`unknown control "${id}"`);
49920
+ if (!Number.isFinite(value)) throw new Error(`control "${id}" must be finite`);
49921
+ out2.set(`control:${id}:value`, Math.max(c2.min, Math.min(c2.max, value)));
49922
+ }
49923
+ return out2;
49924
+ }
49925
+ function bracket(keys, value) {
49926
+ let i2 = 0;
49927
+ while (i2 < keys.length - 2 && value > keys[i2 + 1]) i2++;
49928
+ return [i2, Math.max(0, Math.min(1, (value - keys[i2]) / (keys[i2 + 1] - keys[i2])))];
49929
+ }
49930
+ function sampleBinding(binding, values) {
49931
+ const [x2, u2] = bracket(binding.axes[0].keys, values[0]);
49932
+ const [y2, v2] = binding.axes[1] ? bracket(binding.axes[1].keys, values[1]) : [0, 0];
49933
+ const width = binding.axes[0].keys.length, a2 = y2 * width + x2;
49934
+ const mix = (a3, b2, u3) => a3 + (b2 - a3) * u3;
49935
+ return binding.channels.map((ch) => {
49936
+ const read = (i2, j2) => j2 === void 0 ? ch.values[i2] : ch.values[i2][j2];
49937
+ const at = (j2) => {
49938
+ const top = mix(read(a2, j2), read(a2 + 1, j2), u2);
49939
+ return binding.axes[1] ? mix(top, mix(read(a2 + width, j2), read(a2 + width + 1, j2), u2), v2) : top;
49940
+ };
49941
+ return Array.isArray(ch.values[0]) ? ch.values[0].map((_, j2) => at(j2)) : at();
49942
+ });
49943
+ }
49944
+ function resolveControls(doc, input) {
49945
+ if (!doc.controlBindings?.length) return input;
49946
+ const out2 = new Map(input), controls = new Map(doc.controls?.map((c2) => [c2.id, c2]));
49947
+ for (const b2 of doc.controlBindings) {
49948
+ const values = b2.axes.map((axis) => {
49949
+ const c2 = controls.get(axis.control);
49950
+ const value = input.get(`control:${c2.id}:value`) ?? c2.default;
49951
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`control "${c2.id}" must be finite`);
49952
+ return Math.max(c2.min, Math.min(c2.max, value));
49953
+ });
49954
+ const sampled = sampleBinding(b2, values);
49955
+ b2.channels.forEach((ch, i2) => out2.set(`${ch.target}:${ch.id}:${ch.prop}`, sampled[i2]));
49956
+ }
49957
+ return out2;
49958
+ }
49959
+
49843
49960
  // src/rig-anim.ts
49844
49961
  var DEG2 = Math.PI / 180;
49845
49962
  var named = {
@@ -49914,8 +50031,8 @@ ${parts.join("\n")}
49914
50031
  }
49915
50032
  if (Array.isArray(a2.v) && Array.isArray(b2.v) && typeof a2.v[0] === "number") {
49916
50033
  if (track.interpolation === "pchip") return a2.v.map((_, j2) => smoothValue(track, i2, t2, j2, loop));
49917
- const u2 = ease(a2.ease)((t2 - a2.t) / (b2.t - a2.t)), av = a2.v, bv = b2.v;
49918
- return av.map((v2, k2) => v2 + ((bv[k2] ?? v2) - v2) * u2);
50034
+ const phase = (t2 - a2.t) / (b2.t - a2.t), av = a2.v, bv = b2.v;
50035
+ return av.map((v2, k2) => v2 + ((bv[k2] ?? v2) - v2) * ease(a2.componentEase?.[k2] ?? a2.ease)(phase));
49919
50036
  }
49920
50037
  return a2.v;
49921
50038
  }
@@ -49943,6 +50060,7 @@ ${parts.join("\n")}
49943
50060
  }
49944
50061
  function restValue(doc, key) {
49945
50062
  const [target, id, prop] = key.split(":");
50063
+ if (target === "control") return doc.controls?.find((c2) => c2.id === id)?.default ?? 0;
49946
50064
  if (target === "bone") return doc.bones.find((b2) => b2.id === id).transform[prop];
49947
50065
  if (target === "slot") {
49948
50066
  const s2 = doc.slots.find((s3) => s3.id === id);
@@ -50002,6 +50120,13 @@ ${parts.join("\n")}
50002
50120
  if (!parent) return local;
50003
50121
  if (bone.inherit === "normal") return mul(parent, local);
50004
50122
  const [tx, ty] = apply(parent, s2.x, s2.y);
50123
+ if (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection") {
50124
+ const r2 = s2.rotation * DEG2, x2 = parent.a * Math.cos(r2) + parent.c * Math.sin(r2), y2 = parent.b * Math.cos(r2) + parent.d * Math.sin(r2);
50125
+ const length2 = Math.hypot(x2, y2), a2 = length2 > 1e-5 ? x2 / length2 : x2, b2 = length2 > 1e-5 ? y2 / length2 : y2;
50126
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
50127
+ const basis2 = { a: a2, b: b2, c: -b2 * sign, d: a2 * sign, tx: 0, ty: 0 };
50128
+ return { ...mul(basis2, affineOf(0, 0, 0, s2.scaleX, s2.scaleY, s2.shearX, s2.shearY)), tx, ty };
50129
+ }
50005
50130
  return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
50006
50131
  }
50007
50132
  var orderCache = /* @__PURE__ */ new WeakMap();
@@ -50034,18 +50159,20 @@ ${parts.join("\n")}
50034
50159
  shearY: num(pose, pk("bone", id, "shearY"), r2.shearY)
50035
50160
  };
50036
50161
  };
50037
- function updateSubtree(skel, root) {
50162
+ function updateSubtree(skel, root, includeRoot = true) {
50038
50163
  const doc = skel.doc;
50039
50164
  const kids = /* @__PURE__ */ new Map();
50040
50165
  for (const b2 of doc.bones) if (b2.parent) (kids.get(b2.parent) ?? kids.set(b2.parent, []).get(b2.parent)).push(b2);
50041
- const stack = [root];
50166
+ const stack = includeRoot ? [root] : (kids.get(root) ?? []).map((b2) => b2.id);
50042
50167
  while (stack.length) {
50043
50168
  const id = stack.pop(), b2 = boneOf(doc, id), st = skel.bones.get(id);
50044
50169
  st.world = boneWorldOf(b2, st, b2.parent ? skel.bones.get(b2.parent).world : void 0);
50045
50170
  for (const k2 of kids.get(id) ?? []) stack.push(k2.id);
50046
50171
  }
50047
50172
  }
50048
- function evaluate(doc, pose) {
50173
+ function evaluate(doc, pose, modifiers) {
50174
+ pose = resolveControls(doc, pose);
50175
+ if (modifiers) for (const modify of modifiers) pose = modify(new Map(pose));
50049
50176
  const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
50050
50177
  for (const b2 of boneOrder(doc)) {
50051
50178
  const s2 = localOf(doc, pose, b2);
@@ -50070,6 +50197,23 @@ ${parts.join("\n")}
50070
50197
  }
50071
50198
  return skel;
50072
50199
  }
50200
+ function slotClips(skel) {
50201
+ const keyed = skel.pose.get(pk("drawOrder", "order", "order"));
50202
+ const order = Array.isArray(keyed) ? keyed : [...skel.doc.slots].sort((a2, b2) => a2.z - b2.z).map((s2) => s2.id);
50203
+ const masks = /* @__PURE__ */ new Map();
50204
+ let active;
50205
+ for (const id of order) {
50206
+ const att = skel.attachment.get(id);
50207
+ if (!active && att?.type === "path" && att.clipping) {
50208
+ active = att;
50209
+ continue;
50210
+ }
50211
+ if (active) masks.set(id, active.id);
50212
+ if (active?.clipping?.end === id) active = void 0;
50213
+ }
50214
+ for (const s2 of skel.doc.slots) if (s2.clip) masks.set(s2.id, s2.clip);
50215
+ return masks;
50216
+ }
50073
50217
  var cnum = (skel, kind, id, prop, rest) => num(skel.pose, pk(kind, id, prop), typeof rest === "boolean" ? rest ? 1 : 0 : rest);
50074
50218
  var IDENTITY = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
50075
50219
  var wrapRad = (r2) => r2 > Math.PI ? r2 - 2 * Math.PI : r2 < -Math.PI ? r2 + 2 * Math.PI : r2;
@@ -50279,7 +50423,8 @@ ${parts.join("\n")}
50279
50423
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
50280
50424
  c2 = { ...c2, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p2) => [p2, cnum(skel, "transform", c2.id, p2, c2[p2])])) };
50281
50425
  const target = skel.bones.get(c2.target), tw = target.world;
50282
- const offsetRotation = c2.rotation * DEG2, offsetShearY = c2.shearY * DEG2;
50426
+ const handedness = tw.a * tw.d - tw.b * tw.c < 0 ? -1 : 1;
50427
+ const offsetRotation = c2.rotation * DEG2 * (c2.local ? 1 : handedness), offsetShearY = c2.shearY * DEG2 * (c2.local ? 1 : handedness);
50283
50428
  for (const id of c2.bones) {
50284
50429
  const b2 = boneOf(skel.doc, id), st = skel.bones.get(id);
50285
50430
  if (c2.local) {
@@ -50441,6 +50586,7 @@ ${parts.join("\n")}
50441
50586
  const position = cnum(skel, "path", c2.id, "position", c2.position), spacing = cnum(skel, "path", c2.id, "spacing", c2.spacing);
50442
50587
  const tangents = c2.rotateMode === "tangent", scale = c2.rotateMode === "chainScale";
50443
50588
  const bones = c2.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
50589
+ const inputWorlds = bones.map(({ st }) => ({ ...st.world }));
50444
50590
  const spacesCount = tangents ? bones.length : bones.length + 1;
50445
50591
  const spaces = new Array(spacesCount).fill(0), worldLengths = [];
50446
50592
  let sum = 0;
@@ -50475,9 +50621,10 @@ ${parts.join("\n")}
50475
50621
  if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
50476
50622
  }
50477
50623
  let boneX = samples[0].x, boneY = samples[0].y;
50624
+ const solvedWorlds = /* @__PURE__ */ new Map();
50478
50625
  for (let i2 = 0; i2 < bones.length; i2++) {
50479
50626
  const { b: b2, st } = bones[i2];
50480
- let w2 = { ...st.world };
50627
+ let w2 = { ...inputWorlds[i2] };
50481
50628
  w2.tx += (boneX - w2.tx) * mixX;
50482
50629
  w2.ty += (boneY - w2.ty) * mixY;
50483
50630
  const next = samples[i2 + 1] ?? { x: 0, y: 0, angle: 0 };
@@ -50507,7 +50654,11 @@ ${parts.join("\n")}
50507
50654
  const cos = Math.cos(r2), sin = Math.sin(r2);
50508
50655
  w2 = { a: cos * a2 - sin * bb, b: sin * a2 + cos * bb, c: cos * cc - sin * d2, d: sin * cc + cos * d2, tx: w2.tx, ty: w2.ty };
50509
50656
  }
50510
- setEffectiveWorld(skel, b2, st, w2);
50657
+ solvedWorlds.set(b2.id, w2);
50658
+ }
50659
+ for (const b2 of boneOrder(skel.doc)) {
50660
+ const world = solvedWorlds.get(b2.id);
50661
+ if (world) setEffectiveWorld(skel, b2, skel.bones.get(b2.id), world);
50511
50662
  }
50512
50663
  }
50513
50664
  function measuredPathLength(points, closed, lengths, constantSpeed) {
@@ -50536,9 +50687,14 @@ ${parts.join("\n")}
50536
50687
  case "onlyTranslation":
50537
50688
  return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
50538
50689
  case "noRotationOrReflection": {
50539
- const lenX = Math.hypot(parent.a, parent.b) || 1;
50690
+ const lenX = Math.hypot(parent.a, parent.b);
50540
50691
  const det = parent.a * parent.d - parent.b * parent.c;
50541
- return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
50692
+ if (lenX * lenX <= 1e-4) {
50693
+ const lenY = Math.hypot(parent.c, parent.d), ux3 = lenY ? parent.c / lenY : 0, uy3 = lenY ? parent.d / lenY : 0;
50694
+ return { a: lenY * ux3 * ux3, b: lenY * ux3 * uy3, c: lenY * ux3 * uy3, d: lenY * uy3 * uy3, tx: 0, ty: 0 };
50695
+ }
50696
+ const ux2 = parent.a / lenX, uy2 = parent.b / lenX, sy = Math.abs(det) / lenX;
50697
+ return { a: lenX * ux2 * ux2 + sy * uy2 * uy2, b: (lenX - sy) * ux2 * uy2, c: (lenX - sy) * ux2 * uy2, d: lenX * uy2 * uy2 + sy * ux2 * ux2, tx: 0, ty: 0 };
50542
50698
  }
50543
50699
  case "noScale":
50544
50700
  case "noScaleOrReflection": {
@@ -50564,7 +50720,16 @@ ${parts.join("\n")}
50564
50720
  st.scaleY = Math.hypot(lin.c, lin.d);
50565
50721
  st.shearX = 0;
50566
50722
  st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
50567
- updateSubtree(skel, bone.id);
50723
+ if (parent && (bone.inherit === "noScale" || bone.inherit === "noScaleOrReflection")) {
50724
+ const direction = mul(invert({ ...parent, tx: 0, ty: 0 }), { ...world, tx: 0, ty: 0 });
50725
+ st.rotation = Math.atan2(direction.b, direction.a) / DEG2;
50726
+ st.scaleX = Math.hypot(world.a, world.b);
50727
+ st.scaleY = Math.hypot(world.c, world.d);
50728
+ const sign = bone.inherit === "noScale" && parent.a * parent.d - parent.b * parent.c < 0 ? -1 : 1;
50729
+ st.shearY = shortest((Math.atan2(world.d, world.c) - Math.atan2(world.b, world.a)) / DEG2 * sign - 90);
50730
+ }
50731
+ st.world = world;
50732
+ updateSubtree(skel, bone.id, false);
50568
50733
  }
50569
50734
  function attachmentWorldVertices(skel, slotId, att) {
50570
50735
  const deform = skel.pose.get(pk("deform", att.id, "vertices"));
@@ -50850,9 +51015,23 @@ ${parts.join("\n")}
50850
51015
  this.state = new AnimationState(this, { ...opts, mix: opts.mix ?? 0.18 });
50851
51016
  this.setPose(/* @__PURE__ */ new Map());
50852
51017
  }
51018
+ inputPose = /* @__PURE__ */ new Map();
51019
+ controlOverrides = /* @__PURE__ */ new Map();
51020
+ /** Manual inputs override animated controls until resetControls; values are clamped to declared ranges. */
51021
+ setControls(values) {
51022
+ this.controlOverrides = controlPose(this.doc, values);
51023
+ this.setPose(this.inputPose);
51024
+ }
51025
+ resetControls() {
51026
+ this.controlOverrides.clear();
51027
+ this.setPose(this.inputPose);
51028
+ }
50853
51029
  view = new Container();
50854
51030
  state;
50855
51031
  items = [];
51032
+ beforePose = /* @__PURE__ */ new Set();
51033
+ afterPose = /* @__PURE__ */ new Set();
51034
+ clipPoints = /* @__PURE__ */ new Map();
50856
51035
  skel;
50857
51036
  textures;
50858
51037
  pages = /* @__PURE__ */ new Map();
@@ -50861,6 +51040,35 @@ ${parts.join("\n")}
50861
51040
  /** slots the editor has selected; setPose draws them at GHOST_ALPHA at least */
50862
51041
  ghosts = /* @__PURE__ */ new Set();
50863
51042
  clipGraphics = /* @__PURE__ */ new Map();
51043
+ /** Read-only render handles for in-page tools. Nodes remain owned by the player. */
51044
+ get renderItems() {
51045
+ return this.items;
51046
+ }
51047
+ /** Current document-space clipping polygons, keyed by path attachment id. */
51048
+ get clipPolygons() {
51049
+ return this.clipPoints;
51050
+ }
51051
+ /**
51052
+ * Modify resolved control outputs before skeleton evaluation, constraints and skinning.
51053
+ * Callbacks run in registration order, each with its own map. Change control inputs via setControls.
51054
+ */
51055
+ onBeforePose(callback) {
51056
+ this.beforePose.add(callback);
51057
+ return () => {
51058
+ this.beforePose.delete(callback);
51059
+ };
51060
+ }
51061
+ /** Subscribe after the base pose has been rebuilt. Return value removes the callback. */
51062
+ onAfterPose(callback) {
51063
+ this.afterPose.add(callback);
51064
+ return () => {
51065
+ this.afterPose.delete(callback);
51066
+ };
51067
+ }
51068
+ /** Rebuild the current pose without advancing playback; useful for reversible overlays. */
51069
+ refreshPose() {
51070
+ this.setPose(this.inputPose);
51071
+ }
50864
51072
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
50865
51073
  pageTexture(tex) {
50866
51074
  let page = this.pages.get(tex.source);
@@ -50915,8 +51123,12 @@ ${parts.join("\n")}
50915
51123
  }
50916
51124
  /** Apply a pose (rest where silent) to the display objects. */
50917
51125
  setPose(pose) {
50918
- const skel = this.skel = evaluate(this.doc, pose);
50919
- const clips = new Set(this.doc.slots.flatMap((s2) => s2.clip ? [s2.clip] : []));
51126
+ this.inputPose = new Map(pose);
51127
+ if (this.controlOverrides.size) pose = new Map([...pose, ...this.controlOverrides]);
51128
+ this.clipPoints.clear();
51129
+ const skel = this.skel = evaluate(this.doc, pose, this.beforePose);
51130
+ pose = skel.pose;
51131
+ const masks = slotClips(skel), clips = new Set(masks.values());
50920
51132
  for (const [id, g2] of this.clipGraphics) if (!clips.has(id)) {
50921
51133
  g2.destroy();
50922
51134
  this.clipGraphics.delete(id);
@@ -50933,7 +51145,9 @@ ${parts.join("\n")}
50933
51145
  const vertices = attachmentWorldVertices(skel, att.slot, att);
50934
51146
  const n2 = Math.max(32, vertices.length * 4);
50935
51147
  const points = samplePath(vertices, true, att.lengths, true, Array.from({ length: n2 }, (_, i2) => i2 / n2), true);
50936
- g2.clear().poly(points.flatMap((p2) => [p2.x, p2.y]), true).fill(16777215);
51148
+ const polygon = points.flatMap((p2) => [p2.x, p2.y]);
51149
+ this.clipPoints.set(id, polygon);
51150
+ g2.clear().poly(polygon, true).fill(16777215);
50937
51151
  }
50938
51152
  const m2 = new Matrix();
50939
51153
  const orderV = pose.get(pk("drawOrder", "order", "order"));
@@ -50941,7 +51155,8 @@ ${parts.join("\n")}
50941
51155
  if (Array.isArray(orderV)) orderV.forEach((id, i2) => order.set(id, i2));
50942
51156
  for (const it of this.items) {
50943
51157
  const s2 = it.slot, shown = skel.attachment.get(s2.id);
50944
- it.node.mask = s2.clip ? this.clipGraphics.get(s2.clip) ?? null : null;
51158
+ const mask = masks.get(s2.id);
51159
+ it.node.mask = mask ? this.clipGraphics.get(mask) ?? null : null;
50945
51160
  if (shown !== it.att || this.hidden.has(s2.id)) {
50946
51161
  it.node.visible = false;
50947
51162
  continue;
@@ -50954,6 +51169,7 @@ ${parts.join("\n")}
50954
51169
  it.node.tint = rgbOf(color);
50955
51170
  it.node.zIndex = order.size ? order.get(s2.id) ?? 0 : numOf(pose, pk("slot", s2.id, "z"), s2.z);
50956
51171
  if (it.skinned) {
51172
+ it.node.setFromMatrix(m2.identity());
50957
51173
  const v2 = attachmentWorldVertices(skel, s2.id, it.att);
50958
51174
  const buf = it.skinned.mesh.geometry.getBuffer("aPosition");
50959
51175
  buf.data.set(v2);
@@ -50986,6 +51202,7 @@ ${parts.join("\n")}
50986
51202
  it.node.scale.y *= h2 / (this.textures.get(att.asset)?.height ?? h2);
50987
51203
  }
50988
51204
  }
51205
+ for (const callback of this.afterPose) callback(this);
50989
51206
  }
50990
51207
  /** World-space bone positions — used by editor gizmos and renderPreview overlays */
50991
51208
  bonePositions() {
@@ -0,0 +1,37 @@
1
+ import type { RigPlayer } from "./rig-runtime";
2
+ import type { PartTransform } from "./interactive-editor";
3
+ export interface BoneEditPatch {
4
+ format: "golem.bone-edits";
5
+ version: 1;
6
+ documentId: string;
7
+ transforms: Record<string, PartTransform>;
8
+ }
9
+ export interface InteractiveBoneEditorOptions {
10
+ player: Pick<RigPlayer, "doc" | "onBeforePose" | "refreshPose">;
11
+ documentId: string;
12
+ onChange?: (patch: BoneEditPatch) => void;
13
+ onRender?: () => void;
14
+ historyLimit?: number;
15
+ }
16
+ /** Add bone offsets after expression controls, before constraints and native skinning. */
17
+ export declare function createInteractiveBoneEditor(options: InteractiveBoneEditorOptions): {
18
+ getTransform: (id: string) => {
19
+ x: number;
20
+ y: number;
21
+ rotation: number;
22
+ scaleX: number;
23
+ scaleY: number;
24
+ };
25
+ exportPatch: () => BoneEditPatch;
26
+ validatePatch: (value: unknown) => BoneEditPatch;
27
+ readonly canUndo: boolean;
28
+ readonly canRedo: boolean;
29
+ setTransform(id: string, value: Partial<PartTransform>): void;
30
+ importPatch(value: unknown): void;
31
+ reset(id?: string): void;
32
+ setVisible(value: boolean): void;
33
+ undo(): void;
34
+ redo(): void;
35
+ dispose(): void;
36
+ };
37
+ export type InteractiveBoneEditor = ReturnType<typeof createInteractiveBoneEditor>;
@@ -0,0 +1,80 @@
1
+ import type { RigPlayer } from "./rig-runtime";
2
+ export interface EditorPoint {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ export interface PartTransform {
7
+ x: number;
8
+ y: number;
9
+ rotation: number;
10
+ scaleX: number;
11
+ scaleY: number;
12
+ }
13
+ export interface InteractiveEditPatch {
14
+ format: "golem.interactive-edits";
15
+ version: 1;
16
+ documentId: string;
17
+ transforms: Record<string, PartTransform>;
18
+ }
19
+ export interface EditorGroup {
20
+ id: string;
21
+ members: readonly string[];
22
+ pivotBone?: string;
23
+ }
24
+ export type InteractivePlayer = Pick<RigPlayer, "doc" | "view" | "world" | "renderItems" | "clipPolygons" | "onAfterPose" | "refreshPose">;
25
+ export interface InteractiveEditorOptions {
26
+ player: InteractivePlayer;
27
+ /** Transparent overlay aligned with the player's rendered viewport. It is cleared by the editor. */
28
+ overlay: HTMLCanvasElement;
29
+ /** Host-defined stable rig fingerprint; mismatched patches are rejected. */
30
+ documentId: string;
31
+ groups?: readonly EditorGroup[];
32
+ /** Override when overlay backing pixels differ from the renderer's logical coordinates. */
33
+ screenToDocument?: (client: EditorPoint) => EditorPoint;
34
+ documentToOverlay?: (point: EditorPoint) => EditorPoint;
35
+ alphaFallback?: "skip" | "geometry";
36
+ onChange?: (patch: InteractiveEditPatch) => void;
37
+ onSelection?: (id: string | null) => void;
38
+ onHover?: (id: string | null) => void;
39
+ onError?: (error: Error) => void;
40
+ onGestureStart?: () => void;
41
+ /** Render the host scene after a manual refresh; never called from the pose hook. */
42
+ onRender?: () => void;
43
+ historyLimit?: number;
44
+ }
45
+ export interface EditorFrame {
46
+ id: string;
47
+ center: EditorPoint;
48
+ corners: EditorPoint[];
49
+ handles: {
50
+ name: string;
51
+ p: EditorPoint;
52
+ }[];
53
+ }
54
+ /** Embedded part editor. Source documents are never mutated; edits replay after every evaluated pose. */
55
+ export declare function createInteractiveEditor(options: InteractiveEditorOptions): {
56
+ select: (id: string | null) => void;
57
+ hitTest: (p: EditorPoint) => string | null;
58
+ getFrame: (id: string) => EditorFrame | null;
59
+ getTransform: (id: string) => {
60
+ x: number;
61
+ y: number;
62
+ rotation: number;
63
+ scaleX: number;
64
+ scaleY: number;
65
+ };
66
+ readonly selection: string | null;
67
+ readonly canUndo: boolean;
68
+ readonly canRedo: boolean;
69
+ exportPatch: () => InteractiveEditPatch;
70
+ setEnabled(value: boolean): void;
71
+ setVisible(value: boolean): void;
72
+ setTransform(id: string, value: Partial<PartTransform>): void;
73
+ reset(id?: string): void;
74
+ undo(): void;
75
+ redo(): void;
76
+ importPatch(value: unknown): void;
77
+ dispose(): void;
78
+ };
79
+ export type InteractiveEditor = ReturnType<typeof createInteractiveEditor>;
80
+ export { createInteractiveBoneEditor, type BoneEditPatch, type InteractiveBoneEditorOptions } from "./interactive-bones";