@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.
@@ -0,0 +1,744 @@
1
+ // src/interactive-editor.ts
2
+ import { Matrix } from "pixi.js";
3
+
4
+ // src/rig-anim.ts
5
+ var DEG = Math.PI / 180;
6
+ var pk = (target, id, prop) => `${target}:${id}:${prop}`;
7
+
8
+ // src/interactive-bones.ts
9
+ var neutral = () => ({
10
+ x: 0,
11
+ y: 0,
12
+ rotation: 0,
13
+ scaleX: 1,
14
+ scaleY: 1
15
+ });
16
+ var clone = (v) => Object.assign(/* @__PURE__ */ Object.create(null), JSON.parse(JSON.stringify(v)));
17
+ function createInteractiveBoneEditor(options) {
18
+ if (!options.documentId) throw Error("documentId is required");
19
+ const limit = options.historyLimit ?? 100;
20
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e4)
21
+ throw Error("historyLimit must be an integer from 1 to 10000");
22
+ const { player } = options, bones = new Map(player.doc.bones.map((b) => [b.id, b]));
23
+ let transforms = /* @__PURE__ */ Object.create(null), past = [], future = [], visible = true, disposed = false;
24
+ const ensure = () => {
25
+ if (disposed) throw Error("Bone editor is disposed");
26
+ };
27
+ const known = (id) => {
28
+ if (!bones.has(id)) throw Error("Unknown bone: " + id);
29
+ };
30
+ const getTransform = (id) => {
31
+ ensure();
32
+ known(id);
33
+ return { ...neutral(), ...transforms[id] };
34
+ };
35
+ const exportPatch = () => ({
36
+ format: "golem.bone-edits",
37
+ version: 1,
38
+ documentId: options.documentId,
39
+ transforms: clone(transforms)
40
+ });
41
+ const refresh = () => {
42
+ player.refreshPose();
43
+ options.onRender?.();
44
+ };
45
+ const notify = () => {
46
+ refresh();
47
+ options.onChange?.(exportPatch());
48
+ };
49
+ const remember = () => {
50
+ past.push(clone(transforms));
51
+ if (past.length > limit) past.shift();
52
+ future = [];
53
+ };
54
+ const normalize = () => {
55
+ for (const [id, t] of Object.entries(transforms))
56
+ if (Object.entries(t).every(
57
+ ([k, v]) => v === neutral()[k]
58
+ ))
59
+ delete transforms[id];
60
+ };
61
+ function validateTransform(value) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value))
63
+ throw Error("Invalid bone transform");
64
+ const out = neutral();
65
+ for (const [k, v] of Object.entries(value)) {
66
+ if (!Object.hasOwn(out, k) || typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > 1e4 || k.startsWith("scale") && (v < 0.1 || v > 4))
67
+ throw Error("Invalid bone parameter: " + k);
68
+ out[k] = v;
69
+ }
70
+ return out;
71
+ }
72
+ function validatePatch(value) {
73
+ const v = value;
74
+ if (!v || v.format !== "golem.bone-edits" || v.version !== 1 || v.documentId !== options.documentId)
75
+ throw Error("Bone patch format/version/document mismatch");
76
+ if (!v.transforms || typeof v.transforms !== "object" || Array.isArray(v.transforms))
77
+ throw Error("Invalid bone transforms");
78
+ const next = /* @__PURE__ */ Object.create(null);
79
+ for (const [id, t] of Object.entries(v.transforms)) {
80
+ known(id);
81
+ next[id] = validateTransform(t);
82
+ }
83
+ return { ...v, transforms: next };
84
+ }
85
+ function editPose(input) {
86
+ if (!visible || !Object.keys(transforms).length) return input;
87
+ const out = new Map(input);
88
+ for (const [id, t] of Object.entries(transforms)) {
89
+ const bone = bones.get(id);
90
+ for (const [prop, offset] of Object.entries(t)) {
91
+ const k = pk("bone", id, prop), rest = bone.transform[prop] ?? (prop.startsWith("scale") ? 1 : 0), value = out.get(k) ?? rest;
92
+ if (typeof value === "number")
93
+ out.set(
94
+ k,
95
+ prop.startsWith("scale") ? value * offset : value + offset
96
+ );
97
+ }
98
+ for (const ik of player.doc.ik ?? []) {
99
+ const affected = ik.bones.some((bid) => {
100
+ let b = bones.get(bid);
101
+ while (b) {
102
+ if (b.id === id) return true;
103
+ b = b.parent ? bones.get(b.parent) : void 0;
104
+ }
105
+ return false;
106
+ });
107
+ let target = bones.get(ik.target);
108
+ let sharedAncestor = false;
109
+ while (target) {
110
+ if (target.id === id) {
111
+ sharedAncestor = true;
112
+ break;
113
+ }
114
+ target = target.parent ? bones.get(target.parent) : void 0;
115
+ }
116
+ if (affected && !sharedAncestor) out.set(pk("ik", ik.id, "mix"), 0);
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ const unsubscribe = player.onBeforePose(editPose);
122
+ return {
123
+ getTransform,
124
+ exportPatch,
125
+ validatePatch,
126
+ get canUndo() {
127
+ return past.length > 0;
128
+ },
129
+ get canRedo() {
130
+ return future.length > 0;
131
+ },
132
+ setTransform(id, value) {
133
+ ensure();
134
+ known(id);
135
+ const next = validateTransform({ ...getTransform(id), ...value });
136
+ if (JSON.stringify(next) === JSON.stringify(getTransform(id))) return;
137
+ remember();
138
+ transforms[id] = next;
139
+ normalize();
140
+ notify();
141
+ },
142
+ importPatch(value) {
143
+ ensure();
144
+ const next = validatePatch(value);
145
+ remember();
146
+ transforms = next.transforms;
147
+ normalize();
148
+ notify();
149
+ },
150
+ reset(id) {
151
+ ensure();
152
+ if (id !== void 0) known(id);
153
+ if (id !== void 0 ? !transforms[id] : !Object.keys(transforms).length)
154
+ return;
155
+ remember();
156
+ if (id !== void 0) delete transforms[id];
157
+ else transforms = /* @__PURE__ */ Object.create(null);
158
+ notify();
159
+ },
160
+ setVisible(value) {
161
+ ensure();
162
+ visible = value;
163
+ refresh();
164
+ },
165
+ undo() {
166
+ ensure();
167
+ if (!past.length) return;
168
+ future.push(clone(transforms));
169
+ transforms = past.pop();
170
+ notify();
171
+ },
172
+ redo() {
173
+ ensure();
174
+ if (!future.length) return;
175
+ past.push(clone(transforms));
176
+ transforms = future.pop();
177
+ notify();
178
+ },
179
+ dispose() {
180
+ if (disposed) return;
181
+ disposed = true;
182
+ unsubscribe();
183
+ refresh();
184
+ }
185
+ };
186
+ }
187
+
188
+ // src/interactive-editor.ts
189
+ var neutral2 = () => ({
190
+ x: 0,
191
+ y: 0,
192
+ rotation: 0,
193
+ scaleX: 1,
194
+ scaleY: 1
195
+ });
196
+ var copy = (v) => {
197
+ const out = JSON.parse(JSON.stringify(v));
198
+ if (out && typeof out === "object" && !Array.isArray(out))
199
+ Object.setPrototypeOf(out, null);
200
+ return out;
201
+ };
202
+ var mat = (w) => new Matrix(w.a, w.b, w.c, w.d, w.tx, w.ty);
203
+ var rotate = (p, r) => ({
204
+ x: p.x * Math.cos(r) - p.y * Math.sin(r),
205
+ y: p.x * Math.sin(r) + p.y * Math.cos(r)
206
+ });
207
+ var pointIn = (p, v) => {
208
+ let inside = false;
209
+ for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) {
210
+ const x = v[i], y = v[i + 1], xx = v[j], yy = v[j + 1];
211
+ if (y > p.y !== yy > p.y && p.x < (xx - x) * (p.y - y) / (yy - y) + x)
212
+ inside = !inside;
213
+ }
214
+ return inside;
215
+ };
216
+ function createInteractiveEditor(options) {
217
+ const { player, overlay } = options, ctx = overlay.getContext("2d");
218
+ if (!ctx) throw Error("A 2D overlay context is required.");
219
+ if (!options.documentId) throw Error("documentId is required.");
220
+ if (options.historyLimit !== void 0 && (!Number.isInteger(options.historyLimit) || options.historyLimit < 1 || options.historyLimit > 1e4))
221
+ throw Error("historyLimit must be an integer from 1 to 10000");
222
+ const members = /* @__PURE__ */ new Map(), groups = /* @__PURE__ */ new Map(), attachments = new Set(
223
+ player.doc.attachments.filter((a) => a.type !== "path").map((a) => a.id)
224
+ );
225
+ for (const g of options.groups ?? []) {
226
+ if (!g.id || groups.has(g.id) || !g.members.length)
227
+ throw Error("Invalid or duplicate editor group");
228
+ for (const id of g.members) {
229
+ if (!attachments.has(id) || members.has(id))
230
+ throw Error("Unknown or multiply grouped attachment: " + id);
231
+ members.set(id, g.id);
232
+ }
233
+ if (g.pivotBone && !player.doc.bones.some((b) => b.id === g.pivotBone))
234
+ throw Error("Unknown group pivot bone");
235
+ groups.set(g.id, { ...g, members: [...g.members] });
236
+ }
237
+ for (const id of attachments)
238
+ if (!members.has(id)) {
239
+ if (groups.has(id))
240
+ throw Error("Group id collides with an ungrouped attachment");
241
+ groups.set(id, { id, members: [id] });
242
+ members.set(id, id);
243
+ }
244
+ let transforms = /* @__PURE__ */ Object.create(null), past = [], future = [], selected = null, hovered = null, enabled = true, visible = true, disposed = false;
245
+ let drag = null;
246
+ const abort = new AbortController(), prior = {
247
+ pointerEvents: overlay.style.pointerEvents,
248
+ touchAction: overlay.style.touchAction,
249
+ cursor: overlay.style.cursor
250
+ };
251
+ overlay.style.pointerEvents = "auto";
252
+ overlay.style.touchAction = "none";
253
+ const alphaCache = /* @__PURE__ */ new WeakMap();
254
+ const project = (p) => options.documentToOverlay?.(p) ?? player.view.worldTransform.apply(p);
255
+ const unproject = (p) => {
256
+ if (options.screenToDocument) return options.screenToDocument(p);
257
+ const r = overlay.getBoundingClientRect();
258
+ return player.view.worldTransform.clone().invert().apply({
259
+ x: (p.x - r.left) * overlay.width / r.width,
260
+ y: (p.y - r.top) * overlay.height / r.height
261
+ });
262
+ };
263
+ const screen = (p) => {
264
+ const v = project(p), r = overlay.getBoundingClientRect();
265
+ return {
266
+ x: r.left + v.x * r.width / overlay.width,
267
+ y: r.top + v.y * r.height / overlay.height
268
+ };
269
+ };
270
+ const transformOf = (id) => ({ ...neutral2(), ...transforms[id] });
271
+ function ensure() {
272
+ if (disposed) throw Error("Interactive editor is disposed");
273
+ }
274
+ function known(id) {
275
+ if (!groups.has(id)) throw Error("Unknown editor part: " + id);
276
+ }
277
+ function shapes() {
278
+ return player.renderItems.filter(
279
+ (i) => i.node.visible && i.node.alpha > 0 && i.att.type !== "path"
280
+ ).map((it, order) => {
281
+ const n = it.node;
282
+ n.updateLocalTransform();
283
+ let p, uv, ind;
284
+ if ("geometry" in n) {
285
+ p = Array.from(n.geometry.getBuffer("aPosition").data);
286
+ uv = Array.from(n.geometry.getBuffer("aUV").data);
287
+ ind = Array.from(n.geometry.indexBuffer.data);
288
+ } else {
289
+ const t = n.texture, u = t.uvs, a = n.anchor, trim = t.trim, x = (trim?.x ?? 0) - a.x * t.orig.width, y = (trim?.y ?? 0) - a.y * t.orig.height, w = trim?.width ?? t.orig.width, h = trim?.height ?? t.orig.height;
290
+ p = [x, y, x + w, y, x + w, y + h, x, y + h];
291
+ uv = [u.x0, u.y0, u.x1, u.y1, u.x2, u.y2, u.x3, u.y3];
292
+ ind = [0, 1, 2, 0, 2, 3];
293
+ }
294
+ const points = [];
295
+ for (let i = 0; i < p.length; i += 2)
296
+ points.push(n.localTransform.apply({ x: p[i], y: p[i + 1] }));
297
+ return {
298
+ id: members.get(it.att.id),
299
+ points,
300
+ uvs: uv,
301
+ indices: ind,
302
+ node: it.node,
303
+ slot: it.slot,
304
+ z: n.zIndex,
305
+ order
306
+ };
307
+ }).sort((a, b) => a.z - b.z || a.order - b.order);
308
+ }
309
+ function alpha(n, u, v) {
310
+ const source = n.texture.source;
311
+ if (!alphaCache.has(source)) {
312
+ try {
313
+ const resource = source.resource;
314
+ if (!resource) throw Error("Texture has no readable image resource");
315
+ const c = document.createElement("canvas");
316
+ c.width = source.pixelWidth ?? source.width;
317
+ c.height = source.pixelHeight ?? source.height;
318
+ const cx = c.getContext("2d");
319
+ cx.drawImage(resource, 0, 0, c.width, c.height);
320
+ alphaCache.set(source, {
321
+ width: c.width,
322
+ height: c.height,
323
+ data: cx.getImageData(0, 0, c.width, c.height).data
324
+ });
325
+ } catch (e) {
326
+ alphaCache.set(source, null);
327
+ options.onError?.(
328
+ new Error(
329
+ "Texture alpha cannot be read (CORS or unsupported source): " + String(e)
330
+ )
331
+ );
332
+ }
333
+ }
334
+ const a = alphaCache.get(source);
335
+ if (!a) return options.alphaFallback === "geometry";
336
+ if (u < 0 || v < 0 || u > 1 || v > 1) return false;
337
+ const x = u * a.width - 0.5, y = v * a.height - 0.5, x0 = Math.floor(x), y0 = Math.floor(y), fx = x - x0, fy = y - y0;
338
+ let value = 0;
339
+ for (let dy = 0; dy < 2; dy++)
340
+ for (let dx = 0; dx < 2; dx++) {
341
+ const xx = Math.max(0, Math.min(a.width - 1, x0 + dx)), yy = Math.max(0, Math.min(a.height - 1, y0 + dy));
342
+ value += a.data[(yy * a.width + xx) * 4 + 3] * (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy);
343
+ }
344
+ return value >= 24;
345
+ }
346
+ function hitTest(p) {
347
+ ensure();
348
+ const all = shapes();
349
+ for (let s = all.length - 1; s >= 0; s--) {
350
+ const a = all[s], clip = a.slot.clip && player.clipPolygons.get(a.slot.clip);
351
+ if (clip && !pointIn(p, clip)) continue;
352
+ for (let i = 0; i < a.indices.length; i += 3) {
353
+ const ia = a.indices[i], ib = a.indices[i + 1], ic = a.indices[i + 2], A = a.points[ia], B = a.points[ib], C = a.points[ic], den = (B.y - C.y) * (A.x - C.x) + (C.x - B.x) * (A.y - C.y);
354
+ if (Math.abs(den) < 1e-10) continue;
355
+ const wa = ((B.y - C.y) * (p.x - C.x) + (C.x - B.x) * (p.y - C.y)) / den, wb = ((C.y - A.y) * (p.x - C.x) + (A.x - C.x) * (p.y - C.y)) / den, wc = 1 - wa - wb;
356
+ if (Math.min(wa, wb, wc) < -1e-7) continue;
357
+ if (alpha(
358
+ a.node,
359
+ wa * a.uvs[ia * 2] + wb * a.uvs[ib * 2] + wc * a.uvs[ic * 2],
360
+ wa * a.uvs[ia * 2 + 1] + wb * a.uvs[ib * 2 + 1] + wc * a.uvs[ic * 2 + 1]
361
+ ))
362
+ return a.id;
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ function pivot(id) {
368
+ const g = groups.get(id);
369
+ const a = player.doc.attachments.find((a2) => a2.id === g.members[0]), slot = player.doc.slots.find((s) => s.id === a.slot);
370
+ const w = player.world.get(g.pivotBone ?? slot.bone);
371
+ if (!w || !Number.isFinite(w.a * w.d - w.b * w.c) || Math.abs(w.a * w.d - w.b * w.c) < 1e-10)
372
+ return null;
373
+ return mat(w);
374
+ }
375
+ function frame(id) {
376
+ const viewport = overlay.getBoundingClientRect();
377
+ if (!viewport.width || !viewport.height) return null;
378
+ const points = shapes().filter((s) => s.id === id).flatMap((s) => s.points);
379
+ if (!points.length) return null;
380
+ const W = pivot(id);
381
+ if (!W) return null;
382
+ const inv = W.clone().invert(), o = transformOf(id), r = o.rotation * Math.PI / 180, q = points.map((p) => {
383
+ const p0 = inv.apply(p);
384
+ return rotate({ x: p0.x - o.x, y: p0.y - o.y }, -r);
385
+ }), x0 = Math.min(...q.map((p) => p.x)), x1 = Math.max(...q.map((p) => p.x)), y0 = Math.min(...q.map((p) => p.y)), y1 = Math.max(...q.map((p) => p.y)), cx = (x0 + x1) / 2, cy = (y0 + y1) / 2, toWorld = (p) => {
386
+ const v = rotate(p, r);
387
+ return W.apply({ x: v.x + o.x, y: v.y + o.y });
388
+ };
389
+ const handles = [
390
+ ["nw", x0, y0],
391
+ ["n", cx, y0],
392
+ ["ne", x1, y0],
393
+ ["e", x1, cy],
394
+ ["se", x1, y1],
395
+ ["s", cx, y1],
396
+ ["sw", x0, y1],
397
+ ["w", x0, cy]
398
+ ].map(([name, x, y]) => ({
399
+ name: String(name),
400
+ p: toWorld({ x: Number(x), y: Number(y) })
401
+ })), center = toWorld({ x: cx, y: cy }), top = handles[1].p, st = screen(top), sc = screen(center), d = Math.hypot(st.x - sc.x, st.y - sc.y) || 1;
402
+ handles.push({
403
+ name: "rotate",
404
+ p: unproject({
405
+ x: st.x + (st.x - sc.x) / d * 28,
406
+ y: st.y + (st.y - sc.y) / d * 28
407
+ })
408
+ });
409
+ return {
410
+ id,
411
+ W,
412
+ inv,
413
+ o,
414
+ r,
415
+ x0,
416
+ x1,
417
+ y0,
418
+ y1,
419
+ cx,
420
+ cy,
421
+ center,
422
+ top,
423
+ handles,
424
+ corners: [handles[0].p, handles[2].p, handles[4].p, handles[6].p]
425
+ };
426
+ }
427
+ function draw() {
428
+ ctx.clearRect(0, 0, overlay.width, overlay.height);
429
+ if (!enabled || !visible) return;
430
+ const box = overlay.getBoundingClientRect(), sx = overlay.width / (box.width || overlay.width), sy = overlay.height / (box.height || overlay.height);
431
+ const outline = (id, color, controls) => {
432
+ const f = frame(id);
433
+ if (!f) return;
434
+ ctx.strokeStyle = color;
435
+ ctx.lineWidth = 1.5 * Math.min(sx, sy);
436
+ ctx.beginPath();
437
+ f.corners.map(project).forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
438
+ ctx.closePath();
439
+ ctx.stroke();
440
+ if (controls) {
441
+ const top = project(f.top), rot = project(f.handles[8].p);
442
+ ctx.beginPath();
443
+ ctx.moveTo(top.x, top.y);
444
+ ctx.lineTo(rot.x, rot.y);
445
+ ctx.stroke();
446
+ for (const h of f.handles) {
447
+ const p = project(h.p);
448
+ ctx.beginPath();
449
+ h.name === "rotate" ? ctx.ellipse(p.x, p.y, 5 * sx, 5 * sy, 0, 0, Math.PI * 2) : ctx.rect(p.x - 4 * sx, p.y - 4 * sy, 8 * sx, 8 * sy);
450
+ ctx.fillStyle = "#17212c";
451
+ ctx.fill();
452
+ ctx.stroke();
453
+ }
454
+ }
455
+ };
456
+ if (hovered && hovered !== selected) outline(hovered, "#68d5fa", false);
457
+ if (selected) outline(selected, "#ffda81", true);
458
+ }
459
+ function apply() {
460
+ if (!visible) {
461
+ draw();
462
+ return;
463
+ }
464
+ for (const it of player.renderItems) {
465
+ if (!it.node.visible) continue;
466
+ const id = members.get(it.att.id), o = transforms[id];
467
+ if (!o) continue;
468
+ const W = pivot(id);
469
+ if (!W) continue;
470
+ const r = o.rotation * Math.PI / 180, D = W.clone().append(
471
+ new Matrix(
472
+ Math.cos(r) * o.scaleX,
473
+ Math.sin(r) * o.scaleX,
474
+ -Math.sin(r) * o.scaleY,
475
+ Math.cos(r) * o.scaleY,
476
+ o.x,
477
+ o.y
478
+ )
479
+ ).append(W.clone().invert());
480
+ it.node.updateLocalTransform();
481
+ it.node.setFromMatrix(D.append(it.node.localTransform.clone()));
482
+ }
483
+ if (enabled && hovered) {
484
+ for (const it of player.renderItems)
485
+ if (members.get(it.att.id) === hovered && it.node.visible)
486
+ it.node.tint = 12185087;
487
+ }
488
+ draw();
489
+ }
490
+ const unsubscribe = player.onAfterPose(apply);
491
+ const exportPatch = () => ({
492
+ format: "golem.interactive-edits",
493
+ version: 1,
494
+ documentId: options.documentId,
495
+ transforms: copy(transforms)
496
+ });
497
+ function refresh() {
498
+ player.refreshPose();
499
+ draw();
500
+ options.onRender?.();
501
+ }
502
+ function notify() {
503
+ refresh();
504
+ options.onChange?.(exportPatch());
505
+ }
506
+ function normalize() {
507
+ for (const [id, t] of Object.entries(transforms))
508
+ if (Object.entries(t).every(
509
+ ([k, v]) => v === neutral2()[k]
510
+ ))
511
+ delete transforms[id];
512
+ }
513
+ function remember() {
514
+ past.push(copy(transforms));
515
+ if (past.length > (options.historyLimit ?? 100)) past.shift();
516
+ future = [];
517
+ }
518
+ function validateTransform(value) {
519
+ if (!value || typeof value !== "object" || Array.isArray(value))
520
+ throw Error("Invalid transform");
521
+ const out = neutral2();
522
+ for (const [k, v] of Object.entries(value)) {
523
+ if (!Object.hasOwn(out, k) || typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > 1e4 || k.startsWith("scale") && (v < 0.1 || v > 4))
524
+ throw Error("Invalid transform parameter: " + k);
525
+ out[k] = v;
526
+ }
527
+ return out;
528
+ }
529
+ function select(id) {
530
+ ensure();
531
+ if (id !== null) known(id);
532
+ selected = id;
533
+ options.onSelection?.(id);
534
+ draw();
535
+ }
536
+ function handleAt(p, f) {
537
+ return f?.handles.find((h) => {
538
+ const s = screen(h.p);
539
+ return Math.hypot(p.x - s.x, p.y - s.y) < 9;
540
+ });
541
+ }
542
+ const down = (e) => {
543
+ if (!enabled || !visible || e.button !== 0 || drag) return;
544
+ const client = { x: e.clientX, y: e.clientY }, q = unproject(client);
545
+ let f = selected ? frame(selected) : null, h = handleAt(client, f);
546
+ if (!h) {
547
+ select(hitTest(q));
548
+ if (!selected) return;
549
+ f = frame(selected);
550
+ }
551
+ if (!f) return;
552
+ options.onGestureStart?.();
553
+ drag = {
554
+ id: selected,
555
+ mode: h?.name ?? "move",
556
+ f,
557
+ start: f.inv.apply(q),
558
+ client,
559
+ initial: transformOf(selected),
560
+ recorded: false,
561
+ pointerId: e.pointerId
562
+ };
563
+ overlay.setPointerCapture(e.pointerId);
564
+ e.preventDefault();
565
+ };
566
+ const move = (e) => {
567
+ if (!enabled || !visible || drag && drag.pointerId !== e.pointerId)
568
+ return;
569
+ const client = { x: e.clientX, y: e.clientY }, q = unproject(client);
570
+ if (!drag) {
571
+ const f2 = selected ? frame(selected) : null, h = handleAt(client, f2), next = h ? null : hitTest(q);
572
+ if (hovered !== next) {
573
+ hovered = next;
574
+ options.onHover?.(next);
575
+ }
576
+ if (h && f2) {
577
+ const a = screen(h.p), c = screen(f2.center), angle = (Math.atan2(a.y - c.y, a.x - c.x) * 180 / Math.PI + 180) % 180;
578
+ overlay.style.cursor = h.name === "rotate" ? "crosshair" : ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"][Math.round(angle / 45) % 4];
579
+ } else overlay.style.cursor = hovered ? "move" : "default";
580
+ refresh();
581
+ return;
582
+ }
583
+ const { f, initial: o } = drag;
584
+ if (!drag.recorded) {
585
+ if (Math.hypot(e.clientX - drag.client.x, e.clientY - drag.client.y) < 0.35)
586
+ return;
587
+ remember();
588
+ drag.recorded = true;
589
+ }
590
+ const p = f.inv.apply(q);
591
+ let n = { ...o };
592
+ if (drag.mode === "move") {
593
+ n.x += p.x - drag.start.x;
594
+ n.y += p.y - drag.start.y;
595
+ } else if (drag.mode === "rotate") {
596
+ const c = f.inv.apply(f.center), a0 = Math.atan2(drag.start.y - c.y, drag.start.x - c.x), a1 = Math.atan2(p.y - c.y, p.x - c.x);
597
+ n.rotation = o.rotation + Math.atan2(Math.sin(a1 - a0), Math.cos(a1 - a0)) * 180 / Math.PI;
598
+ const v = rotate({ x: f.cx, y: f.cy }, n.rotation * Math.PI / 180);
599
+ n.x = c.x - v.x;
600
+ n.y = c.y - v.y;
601
+ } else {
602
+ const mode = drag.mode, p0 = rotate({ x: p.x - o.x, y: p.y - o.y }, -f.r), s0 = rotate({ x: drag.start.x - o.x, y: drag.start.y - o.y }, -f.r), ax = mode.includes("e") ? f.x0 : mode.includes("w") ? f.x1 : f.cx, ay = mode.includes("s") ? f.y0 : mode.includes("n") ? f.y1 : f.cy;
603
+ let fx = /[ew]/.test(mode) ? (p0.x - ax) / (s0.x - ax || 1) : 1, fy = /[ns]/.test(mode) ? (p0.y - ay) / (s0.y - ay || 1) : 1;
604
+ n.scaleX = Math.max(0.1, Math.min(4, o.scaleX * fx));
605
+ n.scaleY = Math.max(0.1, Math.min(4, o.scaleY * fy));
606
+ fx = n.scaleX / o.scaleX;
607
+ fy = n.scaleY / o.scaleY;
608
+ const a = rotate({ x: ax, y: ay }, f.r), b = rotate({ x: ax * fx, y: ay * fy }, f.r);
609
+ n.x = o.x + a.x - b.x;
610
+ n.y = o.y + a.y - b.y;
611
+ }
612
+ for (const k of ["x", "y", "rotation"])
613
+ n[k] = Number.isFinite(n[k]) ? Math.max(-1e4, Math.min(1e4, n[k])) : o[k];
614
+ transforms[drag.id] = validateTransform(n);
615
+ normalize();
616
+ notify();
617
+ };
618
+ const end = (event) => {
619
+ if (!drag || event && event.pointerId !== drag.pointerId) return;
620
+ const id = drag.pointerId;
621
+ drag = null;
622
+ if (overlay.hasPointerCapture(id)) overlay.releasePointerCapture(id);
623
+ };
624
+ const listen = (name, fn) => overlay.addEventListener(name, fn, { signal: abort.signal });
625
+ listen("pointerdown", down);
626
+ listen("pointermove", move);
627
+ listen("pointerup", end);
628
+ listen("pointercancel", end);
629
+ listen("lostpointercapture", end);
630
+ listen("pointerleave", () => {
631
+ if (!drag) {
632
+ hovered = null;
633
+ options.onHover?.(null);
634
+ refresh();
635
+ }
636
+ });
637
+ refresh();
638
+ return {
639
+ select,
640
+ hitTest,
641
+ getFrame: (id) => {
642
+ ensure();
643
+ known(id);
644
+ return frame(id);
645
+ },
646
+ getTransform: (id) => {
647
+ ensure();
648
+ known(id);
649
+ return transformOf(id);
650
+ },
651
+ get selection() {
652
+ return selected;
653
+ },
654
+ get canUndo() {
655
+ return past.length > 0;
656
+ },
657
+ get canRedo() {
658
+ return future.length > 0;
659
+ },
660
+ exportPatch,
661
+ setEnabled(value) {
662
+ ensure();
663
+ end();
664
+ enabled = value;
665
+ hovered = null;
666
+ overlay.style.pointerEvents = value ? "auto" : "none";
667
+ refresh();
668
+ },
669
+ setVisible(value) {
670
+ ensure();
671
+ end();
672
+ visible = value;
673
+ refresh();
674
+ },
675
+ setTransform(id, value) {
676
+ ensure();
677
+ known(id);
678
+ const t = validateTransform({ ...transformOf(id), ...value });
679
+ if (JSON.stringify(t) === JSON.stringify(transformOf(id))) return;
680
+ remember();
681
+ transforms[id] = t;
682
+ normalize();
683
+ notify();
684
+ },
685
+ reset(id) {
686
+ ensure();
687
+ if (id !== void 0) known(id);
688
+ if (id !== void 0 ? !transforms[id] : !Object.keys(transforms).length)
689
+ return;
690
+ remember();
691
+ if (id !== void 0) delete transforms[id];
692
+ else transforms = /* @__PURE__ */ Object.create(null);
693
+ notify();
694
+ },
695
+ undo() {
696
+ ensure();
697
+ if (!past.length) return;
698
+ future.push(copy(transforms));
699
+ transforms = past.pop();
700
+ notify();
701
+ },
702
+ redo() {
703
+ ensure();
704
+ if (!future.length) return;
705
+ past.push(copy(transforms));
706
+ transforms = future.pop();
707
+ notify();
708
+ },
709
+ importPatch(value) {
710
+ ensure();
711
+ if (!value || typeof value !== "object") throw Error("Invalid patch");
712
+ const v = value;
713
+ if (v.format !== "golem.interactive-edits" || v.version !== 1 || v.documentId !== options.documentId)
714
+ throw Error("Patch format/version/document mismatch");
715
+ if (!v.transforms || typeof v.transforms !== "object" || Array.isArray(v.transforms))
716
+ throw Error("Invalid transforms");
717
+ const next = /* @__PURE__ */ Object.create(null);
718
+ for (const [id, t] of Object.entries(v.transforms)) {
719
+ known(id);
720
+ next[id] = validateTransform(t);
721
+ }
722
+ remember();
723
+ transforms = next;
724
+ normalize();
725
+ notify();
726
+ },
727
+ dispose() {
728
+ if (disposed) return;
729
+ end();
730
+ disposed = true;
731
+ abort.abort();
732
+ unsubscribe();
733
+ player.refreshPose();
734
+ ctx.clearRect(0, 0, overlay.width, overlay.height);
735
+ Object.assign(overlay.style, prior);
736
+ options.onRender?.();
737
+ }
738
+ };
739
+ }
740
+ export {
741
+ createInteractiveBoneEditor,
742
+ createInteractiveEditor
743
+ };
744
+ //# sourceMappingURL=interactive-editor.js.map