@lancercomet/zoom-pan 0.1.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,1194 @@
1
+ const b = async (d, t) => new Promise((e, s) => {
2
+ const i = new Image();
3
+ typeof d == "string" ? (t !== void 0 && (i.crossOrigin = t), i.src = d) : i.src = URL.createObjectURL(d), i.onload = () => {
4
+ e(i);
5
+ }, i.onerror = () => {
6
+ s(new Error("Image load failed"));
7
+ };
8
+ }), M = (d, t, e) => Math.min(Math.max(d, t), e);
9
+ class B {
10
+ canvas;
11
+ context;
12
+ contentCanvas;
13
+ contentContext;
14
+ topScreenCanvas;
15
+ topScreenContext;
16
+ _render;
17
+ _options;
18
+ _resizeObserver;
19
+ _layerManagers = [];
20
+ _plugins = /* @__PURE__ */ new Map();
21
+ _isResetting = !1;
22
+ _isResizing = !1;
23
+ _resizeReleaseTimer;
24
+ // Dirty flag - only render when needed
25
+ _needsRender = !0;
26
+ _raf = 0;
27
+ _lastFrameTs = performance.now();
28
+ // Pan state (in CSS px world coords)
29
+ _tx = 0;
30
+ _ty = 0;
31
+ // Zoom anchor (CSS px in canvas client coords)
32
+ _anchorX = 0;
33
+ _anchorY = 0;
34
+ // --------- Zoom ----------
35
+ _currentLogZ = Math.log(1);
36
+ _targetLogZ = Math.log(1);
37
+ LOG_MIN;
38
+ LOG_MAX;
39
+ // --------- Callbacks ----------
40
+ _updateCallbacks = /* @__PURE__ */ new Set();
41
+ _beforeRenderCallbacks = /* @__PURE__ */ new Set();
42
+ _afterRenderCallbacks = /* @__PURE__ */ new Set();
43
+ get zoom() {
44
+ return Math.exp(this._currentLogZ);
45
+ }
46
+ get minZoom() {
47
+ return this._options.minZoom;
48
+ }
49
+ get maxZoom() {
50
+ return this._options.maxZoom;
51
+ }
52
+ // -------- Dpr --------
53
+ _dpr = Math.max(1, window.devicePixelRatio || 1);
54
+ get dpr() {
55
+ return this._dpr;
56
+ }
57
+ // --------- Plugin System ----------
58
+ /**
59
+ * Install a plugin.
60
+ */
61
+ use(t) {
62
+ return this._plugins.has(t.name) ? (console.warn(`Plugin "${t.name}" is already installed.`), t) : (this._plugins.set(t.name, t), t.install(this), t);
63
+ }
64
+ /**
65
+ * Uninstall a plugin by name.
66
+ */
67
+ unuse(t) {
68
+ const e = this._plugins.get(t);
69
+ e && (e.destroy(), this._plugins.delete(t));
70
+ }
71
+ /**
72
+ * Get an installed plugin by name.
73
+ */
74
+ getPlugin(t) {
75
+ return this._plugins.get(t);
76
+ }
77
+ // --------- Callbacks ----------
78
+ /**
79
+ * Register a callback to be called every frame with delta time.
80
+ */
81
+ onUpdate(t) {
82
+ this._updateCallbacks.add(t);
83
+ }
84
+ /**
85
+ * Unregister an update callback.
86
+ */
87
+ offUpdate(t) {
88
+ this._updateCallbacks.delete(t);
89
+ }
90
+ /**
91
+ * Register a callback to be called before rendering (with world transform applied).
92
+ */
93
+ onBeforeRender(t) {
94
+ this._beforeRenderCallbacks.add(t);
95
+ }
96
+ /**
97
+ * Unregister a before-render callback.
98
+ */
99
+ offBeforeRender(t) {
100
+ this._beforeRenderCallbacks.delete(t);
101
+ }
102
+ /**
103
+ * Register a callback to be called after rendering.
104
+ */
105
+ onAfterRender(t) {
106
+ this._afterRenderCallbacks.add(t);
107
+ }
108
+ /**
109
+ * Unregister an after-render callback.
110
+ */
111
+ offAfterRender(t) {
112
+ this._afterRenderCallbacks.delete(t);
113
+ }
114
+ /**
115
+ * Request a render on the next frame.
116
+ * Call this when content changes and needs to be redrawn.
117
+ */
118
+ requestRender() {
119
+ this._needsRender = !0;
120
+ }
121
+ // --------- Zoom Methods ----------
122
+ /**
123
+ * Clamp target zoom (log space) to range.
124
+ */
125
+ _clampLog(t) {
126
+ return M(t, this.LOG_MIN, this.LOG_MAX);
127
+ }
128
+ /**
129
+ * Set target zoom at screen anchor point, with smooth transition.
130
+ */
131
+ _setTargetLogZoomAtScreen(t, e, s) {
132
+ Number.isFinite(s) && (this._anchorX = t, this._anchorY = e, this._targetLogZ = this._clampLog(s), this._needsRender = !0);
133
+ }
134
+ /**
135
+ * Zoom to absolute zoom level at screen point, with smooth transition.
136
+ */
137
+ zoomToAtScreen(t, e, s) {
138
+ this._setTargetLogZoomAtScreen(t, e, Math.log(s));
139
+ }
140
+ /**
141
+ * Zoom to absolute zoom level at screen point, instantly (no animation).
142
+ */
143
+ zoomToAtScreenRaw(t, e, s) {
144
+ if (!Number.isFinite(s))
145
+ return;
146
+ const i = Math.max(1e-8, this._options.minZoom), n = this._options.maxZoom, a = M(s, i, n), h = Math.exp(this._currentLogZ), o = a;
147
+ if (!Number.isFinite(h) || h <= 0 || Math.abs(o - h) < 1e-12)
148
+ return;
149
+ const r = Math.log(a);
150
+ this._currentLogZ = r, this._targetLogZ = r;
151
+ const c = o / h;
152
+ this._tx = t - (t - this._tx) * c, this._ty = e - (e - this._ty) * c, this._needsRender = !0;
153
+ }
154
+ /**
155
+ * Zoom to absolute zoom level at world point, with smooth transition.
156
+ */
157
+ zoomToAtWorld(t, e, s) {
158
+ const { x: i, y: n } = this.toScreen(t, e);
159
+ this.zoomToAtScreen(i, n, s);
160
+ }
161
+ /**
162
+ * Zoom by multiplicative factor at screen point, with smooth transition.
163
+ */
164
+ zoomByFactorAtScreen(t, e, s) {
165
+ if (s <= 0 || !Number.isFinite(s))
166
+ return;
167
+ const i = Math.log(s);
168
+ this._setTargetLogZoomAtScreen(t, e, this._targetLogZ + i);
169
+ }
170
+ /**
171
+ * Zoom by log step at screen point. Used by interaction plugin.
172
+ */
173
+ zoomByLogAtScreen(t, e, s) {
174
+ this._setTargetLogZoomAtScreen(t, e, this._targetLogZ + s);
175
+ }
176
+ /**
177
+ * Zoom by multiplicative factor at world point, with smooth transition.
178
+ */
179
+ zoomByFactorAtWorld(t, e, s) {
180
+ const { x: i, y: n } = this.toScreen(t, e);
181
+ this.zoomByFactorAtScreen(i, n, s);
182
+ }
183
+ zoomInAtCenter() {
184
+ const t = this.canvas.getBoundingClientRect(), e = t.width / 2, s = t.height / 2;
185
+ this.zoomByFactorAtScreen(e, s, 1.2);
186
+ }
187
+ zoomOutAtCenter() {
188
+ const t = this.canvas.getBoundingClientRect(), e = t.width / 2, s = t.height / 2, i = 1 / 1.2;
189
+ this.zoomByFactorAtScreen(e, s, i);
190
+ }
191
+ // --------- Pan Methods ----------
192
+ /**
193
+ * Pan by delta (CSS px).
194
+ */
195
+ panBy(t, e) {
196
+ this._tx += t, this._ty += e, this._needsRender = !0;
197
+ }
198
+ /**
199
+ * Set pan position (CSS px).
200
+ */
201
+ setPan(t, e) {
202
+ this._tx = t, this._ty = e, this._needsRender = !0;
203
+ }
204
+ /**
205
+ * Set full transform: zoom and pan.
206
+ */
207
+ setTransform(t, e, s) {
208
+ const i = M(t, this._options.minZoom, this._options.maxZoom);
209
+ this._currentLogZ = Math.log(i), this._targetLogZ = this._currentLogZ, this._tx = e, this._ty = s, this._needsRender = !0;
210
+ }
211
+ // ---------- Internals ----------
212
+ _ensureOffscreenSizeLike(t, e) {
213
+ (t.width !== e.width || t.height !== e.height) && (t.width = e.width, t.height = e.height);
214
+ }
215
+ _loop() {
216
+ if (this._isResizing) {
217
+ this._raf = requestAnimationFrame(() => this._loop());
218
+ return;
219
+ }
220
+ const t = performance.now(), e = Math.max(1, t - this._lastFrameTs);
221
+ this._lastFrameTs = t;
222
+ const { approachKZoom: s } = this._options, i = Math.exp(this._currentLogZ), n = this._targetLogZ - this._currentLogZ, a = Math.abs(n) > 1e-6;
223
+ if (a) {
224
+ const l = 1 - Math.exp(-s * e);
225
+ this._currentLogZ += n * l;
226
+ }
227
+ const h = Math.exp(this._currentLogZ);
228
+ if (h !== i) {
229
+ const l = this._anchorX, u = this._anchorY, p = h / i;
230
+ this._tx = l - (l - this._tx) * p, this._ty = u - (u - this._ty) * p;
231
+ }
232
+ if (this._isResetting) {
233
+ const l = 1 - Math.exp(-this._options.approachKPan * e);
234
+ this._tx += (0 - this._tx) * l, this._ty += (0 - this._ty) * l;
235
+ const u = Math.abs(this._currentLogZ) < 1e-3 && Math.abs(this._targetLogZ) < 1e-6, p = Math.abs(this._tx) < 0.5 && Math.abs(this._ty) < 0.5;
236
+ u && p && (this._currentLogZ = 0, this._targetLogZ = 0, this._tx = 0, this._ty = 0, this._isResetting = !1);
237
+ }
238
+ for (const l of this._updateCallbacks)
239
+ l(e);
240
+ if (!(this._needsRender || a || this._isResetting)) {
241
+ this._raf = requestAnimationFrame(() => this._loop());
242
+ return;
243
+ }
244
+ this._needsRender = !1;
245
+ const r = this.contentCanvas, c = this.contentContext, _ = this.topScreenCanvas, m = this.topScreenContext, g = this.canvas, f = this.context;
246
+ this._ensureOffscreenSizeLike(r, g), this._ensureOffscreenSizeLike(_, g), c.setTransform(1, 0, 0, 1, 0, 0), m.setTransform(1, 0, 0, 1, 0, 0), f.setTransform(1, 0, 0, 1, 0, 0);
247
+ const y = this._options.background;
248
+ typeof y == "string" && y.trim() !== "" && y.toLowerCase() !== "transparent" ? (f.fillStyle = y, f.fillRect(0, 0, g.width, g.height)) : f.clearRect(0, 0, g.width, g.height), c.clearRect(0, 0, r.width, r.height), m.clearRect(0, 0, _.width, _.height), c.setTransform(this._dpr * h, 0, 0, this._dpr * h, this._dpr * this._tx, this._dpr * this._ty);
249
+ for (const l of this._beforeRenderCallbacks)
250
+ l(c);
251
+ this._render(this);
252
+ for (const l of this._afterRenderCallbacks)
253
+ l(c);
254
+ f.drawImage(r, 0, 0), f.drawImage(_, 0, 0), this._raf = requestAnimationFrame(() => this._loop());
255
+ }
256
+ // --------- Transform Helpers ----------
257
+ applyWorldTransform(t) {
258
+ const e = Math.exp(this._currentLogZ);
259
+ t.setTransform(this._dpr * e, 0, 0, this._dpr * e, this._dpr * this._tx, this._dpr * this._ty);
260
+ }
261
+ applyScreenTransform(t) {
262
+ t.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
263
+ }
264
+ // --------- Color Picking ----------
265
+ getPixelColorAtScreen(t, e) {
266
+ const s = Math.floor(t * this._dpr), i = Math.floor(e * this._dpr);
267
+ if (s < 0 || i < 0 || s >= this.canvas.width || i >= this.canvas.height)
268
+ return { r: 0, g: 0, b: 0, a: 0, rgba: "rgba(0,0,0,0)", hex: "#000000" };
269
+ const a = this.contentContext.getImageData(s, i, 1, 1).data, h = a[0], o = a[1], r = a[2], c = a[3] / 255, _ = (g) => g.toString(16).padStart(2, "0"), m = `#${_(h)}${_(o)}${_(r)}`;
270
+ return {
271
+ r: h,
272
+ g: o,
273
+ b: r,
274
+ a: c,
275
+ rgba: `rgba(${h},${o},${r},${c.toFixed(3)})`,
276
+ hex: m
277
+ };
278
+ }
279
+ getPixelColorAtWorld(t, e) {
280
+ const { x: s, y: i } = this.toScreen(t, e);
281
+ return this.getPixelColorAtScreen(s, i);
282
+ }
283
+ // ---------- Public API ----------
284
+ registerLayerManager(t) {
285
+ t && (this._layerManagers.includes(t) || (this._layerManagers.push(t), this._needsRender = !0));
286
+ }
287
+ unregisterLayerManager(t) {
288
+ const e = this._layerManagers.indexOf(t);
289
+ e >= 0 && (this._layerManagers.splice(e, 1), this._needsRender = !0);
290
+ }
291
+ getLayerManagers() {
292
+ return [...this._layerManagers];
293
+ }
294
+ /**
295
+ * Smoothly reset to zoom=1, pan=(0,0).
296
+ */
297
+ resetSmooth() {
298
+ this._isResetting = !0, this._targetLogZ = 0, this._needsRender = !0;
299
+ }
300
+ /**
301
+ * Instantly reset (no animation).
302
+ */
303
+ resetInstant() {
304
+ this._currentLogZ = 0, this._targetLogZ = 0, this._tx = 0, this._ty = 0, this._needsRender = !0;
305
+ }
306
+ /**
307
+ * Convert screen (canvas client) -> world.
308
+ */
309
+ toWorld(t, e) {
310
+ const s = Math.exp(this._currentLogZ), i = (t - this._tx) / s, n = (e - this._ty) / s;
311
+ return { wx: i, wy: n };
312
+ }
313
+ /**
314
+ * Convert world -> screen (canvas client).
315
+ */
316
+ toScreen(t, e) {
317
+ const s = Math.exp(this._currentLogZ);
318
+ return { x: t * s + this._tx, y: e * s + this._ty };
319
+ }
320
+ /**
321
+ * Get current transform.
322
+ */
323
+ getTransform() {
324
+ return { zoom: Math.exp(this._currentLogZ), tx: this._tx, ty: this._ty };
325
+ }
326
+ /**
327
+ * Get viewport bounds in world coordinates.
328
+ */
329
+ getViewportBounds() {
330
+ const t = Math.exp(this._currentLogZ), e = this.canvas.width / this._dpr, s = this.canvas.height / this._dpr, i = -this._tx / t, n = -this._ty / t, a = (e - this._tx) / t, h = (s - this._ty) / t;
331
+ return { left: i, top: n, right: a, bottom: h, width: a - i, height: h - n };
332
+ }
333
+ /**
334
+ * Set zoom range.
335
+ */
336
+ setZoomRange(t, e) {
337
+ this._options.minZoom = t, this._options.maxZoom = e, this.LOG_MIN = Math.log(t), this.LOG_MAX = Math.log(e), this._targetLogZ = Math.min(this.LOG_MAX, Math.max(this.LOG_MIN, this._targetLogZ));
338
+ }
339
+ /**
340
+ * Resize canvas to match parent size and DPR.
341
+ */
342
+ resizeToParent() {
343
+ this._isResizing = !0;
344
+ const e = (this.canvas.parentElement || this.canvas).getBoundingClientRect();
345
+ this._dpr = Math.max(1, window.devicePixelRatio || 1);
346
+ const s = Math.max(1, Math.round(e.width)), i = Math.max(1, Math.round(e.height));
347
+ this.canvas.width = Math.round(s * this._dpr), this.canvas.height = Math.round(i * this._dpr), this.canvas.style.width = `${s}px`, this.canvas.style.height = `${i}px`, this._ensureOffscreenSizeLike(this.contentCanvas, this.canvas), this._ensureOffscreenSizeLike(this.topScreenCanvas, this.canvas), clearTimeout(this._resizeReleaseTimer), this._resizeReleaseTimer = window.setTimeout(() => {
348
+ this._isResizing = !1;
349
+ }, 50);
350
+ }
351
+ /**
352
+ * Destroy and cleanup.
353
+ */
354
+ destroy() {
355
+ cancelAnimationFrame(this._raf);
356
+ for (const t of this._plugins.values())
357
+ t.destroy();
358
+ this._plugins.clear(), this._resizeObserver && this._resizeObserver.disconnect(), this._layerManagers = [], this._updateCallbacks.clear(), this._beforeRenderCallbacks.clear(), this._afterRenderCallbacks.clear();
359
+ }
360
+ constructor(t, e, s) {
361
+ const i = t.getContext("2d", {
362
+ willReadFrequently: !0,
363
+ alpha: !0
364
+ });
365
+ if (!i)
366
+ throw new Error("2D context not available");
367
+ this.canvas = t, this.context = i, this._render = e, this.contentCanvas = document.createElement("canvas"), this.contentCanvas.width = t.width, this.contentCanvas.height = t.height, this.contentContext = this.contentCanvas.getContext("2d", {
368
+ alpha: !0,
369
+ willReadFrequently: !0
370
+ }), this.topScreenCanvas = document.createElement("canvas"), this.topScreenCanvas.width = t.width, this.topScreenCanvas.height = t.height, this.topScreenContext = this.topScreenCanvas.getContext("2d", {
371
+ alpha: !0
372
+ }), this._options = {
373
+ minZoom: 0.5,
374
+ maxZoom: 10,
375
+ approachKZoom: 0.022,
376
+ approachKPan: 0.022,
377
+ autoResize: !0,
378
+ background: "#fff",
379
+ ...s
380
+ }, this.LOG_MIN = Math.log(this._options.minZoom), this.LOG_MAX = Math.log(this._options.maxZoom), this._options.autoResize && (this._resizeObserver = new ResizeObserver(() => this.resizeToParent()), this._resizeObserver.observe(this.canvas.parentElement || this.canvas)), this.resizeToParent(), this._lastFrameTs = performance.now(), this._raf = requestAnimationFrame(() => this._loop());
381
+ }
382
+ }
383
+ let R = 0;
384
+ class z {
385
+ id;
386
+ type;
387
+ name;
388
+ space = "world";
389
+ visible = !0;
390
+ opacity = 1;
391
+ blend = "source-over";
392
+ constructor(t, e, s = "world") {
393
+ this.name = t, this.id = `layer_${e}_${++R}`, this.type = e, this.space = s;
394
+ }
395
+ }
396
+ class C extends z {
397
+ _redraw;
398
+ canvas;
399
+ context;
400
+ x = 0;
401
+ y = 0;
402
+ scale = 1;
403
+ rotation = 0;
404
+ anchor = "topLeft";
405
+ // Drawing state
406
+ _drawing = !1;
407
+ _lastX = 0;
408
+ _lastY = 0;
409
+ /**
410
+ * Begin a stroke at the given world coordinates.
411
+ */
412
+ beginStroke(t, e) {
413
+ const { lx: s, ly: i } = this.toLocalPoint(t, e);
414
+ this._lastX = s, this._lastY = i, this._drawing = !0;
415
+ }
416
+ /**
417
+ * Continue a stroke to the given world coordinates.
418
+ */
419
+ stroke(t, e, s, i, n = 1, a = "brush") {
420
+ if (!this._drawing)
421
+ return;
422
+ const { lx: h, ly: o } = this.toLocalPoint(t, e);
423
+ this.context.beginPath(), this.context.moveTo(this._lastX, this._lastY), this.context.lineTo(h, o), a === "eraser" ? (this.context.globalCompositeOperation = "destination-out", this.context.strokeStyle = "rgba(0, 0, 0, 1)") : (this.context.globalCompositeOperation = "source-over", this.context.strokeStyle = s), this.context.lineWidth = i * n, this.context.lineCap = "round", this.context.lineJoin = "round", this.context.stroke(), this.context.closePath(), this._lastX = h, this._lastY = o;
424
+ }
425
+ /**
426
+ * End the current stroke.
427
+ */
428
+ endStroke() {
429
+ this._drawing = !1;
430
+ }
431
+ /**
432
+ * Check if currently drawing.
433
+ */
434
+ isDrawing() {
435
+ return this._drawing;
436
+ }
437
+ // ============ Snapshot API for undo/redo ============
438
+ /**
439
+ * Capture a snapshot of the canvas content.
440
+ * If region is provided, only that region is captured (more memory efficient).
441
+ * Returns null if capture fails.
442
+ */
443
+ captureSnapshot(t) {
444
+ try {
445
+ if (t) {
446
+ const { x: e, y: s, width: i, height: n } = t, a = Math.max(0, Math.floor(e)), h = Math.max(0, Math.floor(s)), o = Math.min(this.canvas.width - a, Math.ceil(i)), r = Math.min(this.canvas.height - h, Math.ceil(n));
447
+ return o <= 0 || r <= 0 ? null : this.context.getImageData(a, h, o, r);
448
+ }
449
+ return this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ /**
455
+ * Restore a snapshot to the canvas.
456
+ * If position is provided, the snapshot is placed at that position.
457
+ * Otherwise, it's placed at (0, 0).
458
+ */
459
+ restoreSnapshot(t, e) {
460
+ const s = e?.x ?? 0, i = e?.y ?? 0;
461
+ this.context.putImageData(t, s, i);
462
+ }
463
+ /**
464
+ * Clear a region of the canvas.
465
+ * If no region is provided, clears the entire canvas.
466
+ */
467
+ clearRegion(t) {
468
+ t ? this.context.clearRect(t.x, t.y, t.width, t.height) : this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
469
+ }
470
+ // ============ Other methods ============
471
+ /**
472
+ * Request a redraw of the offscreen canvas.
473
+ * This will call the redraw function you provided in the constructor.
474
+ */
475
+ requestRedraw() {
476
+ this._redraw?.(this.context, this.canvas);
477
+ }
478
+ /**
479
+ * Draw an image onto the offscreen canvas.
480
+ */
481
+ drawImage(t, e, s, i, n) {
482
+ this.context.drawImage(t, e, s, i ?? t.width, n ?? t.height);
483
+ }
484
+ /**
485
+ * Hit test the layer.
486
+ * Returns true if the point (wx, wy) in world space is within the layer bounds.
487
+ */
488
+ hitTest(t, e) {
489
+ const { lx: s, ly: i } = this.toLocalPoint(t, e);
490
+ return s >= 0 && s <= this.canvas.width && i >= 0 && i <= this.canvas.height;
491
+ }
492
+ /**
493
+ * Convert a point from world space to local layer space.
494
+ */
495
+ toLocalPoint(t, e) {
496
+ const s = t - this.x, i = e - this.y, n = Math.cos(-this.rotation), a = Math.sin(-this.rotation), h = s * n - i * a, o = s * a + i * n, r = h / this.scale, c = o / this.scale, _ = this.anchor === "center" ? this.canvas.width / 2 : 0, m = this.anchor === "center" ? this.canvas.height / 2 : 0;
497
+ return {
498
+ lx: r + _,
499
+ ly: c + m
500
+ };
501
+ }
502
+ /**
503
+ * Render the layer onto the given context.
504
+ */
505
+ render(t, e) {
506
+ if (!this.visible)
507
+ return;
508
+ const s = this.canvas.width * this.scale, i = this.canvas.height * this.scale, n = this.anchor === "center" ? -s / 2 : 0, a = this.anchor === "center" ? -i / 2 : 0;
509
+ t.save(), t.globalAlpha = this.opacity, t.globalCompositeOperation = this.blend, t.translate(this.x, this.y), t.rotate(this.rotation), t.drawImage(this.canvas, n, a, s, i), t.restore();
510
+ }
511
+ cropTo(t) {
512
+ const e = Math.max(1, Math.floor(t.width)), s = Math.max(1, Math.floor(t.height));
513
+ if (e === this.canvas.width && s === this.canvas.height)
514
+ return;
515
+ const i = this._cloneCanvas(), n = Math.min(i.width, e), a = Math.min(i.height, s);
516
+ this._setCanvasSize(e, s), n > 0 && a > 0 && this.context.drawImage(i, 0, 0, n, a, 0, 0, n, a);
517
+ }
518
+ resizeTo(t) {
519
+ const e = Math.max(1, Math.floor(t.width)), s = Math.max(1, Math.floor(t.height));
520
+ if (e === this.canvas.width && s === this.canvas.height)
521
+ return;
522
+ const i = this._cloneCanvas();
523
+ this._setCanvasSize(e, s), i.width > 0 && i.height > 0 && this.context.drawImage(i, 0, 0, i.width, i.height, 0, 0, e, s);
524
+ }
525
+ _cloneCanvas() {
526
+ const t = document.createElement("canvas");
527
+ if (t.width = this.canvas.width, t.height = this.canvas.height, t.width === 0 || t.height === 0)
528
+ return t;
529
+ const e = t.getContext("2d");
530
+ if (!e)
531
+ throw new Error("Offscreen 2D context unavailable");
532
+ return e.drawImage(this.canvas, 0, 0), t;
533
+ }
534
+ _setCanvasSize(t, e) {
535
+ this.canvas.width = t, this.canvas.height = e, this.context.setTransform(1, 0, 0, 1, 0, 0), this.context.clearRect(0, 0, t, e);
536
+ }
537
+ destroy() {
538
+ this.canvas.width = 0, this.canvas.height = 0;
539
+ }
540
+ constructor(t) {
541
+ super(
542
+ t.name || "",
543
+ "canvas",
544
+ t.space ?? "world"
545
+ ), this.canvas = document.createElement("canvas"), this.canvas.width = t.width, this.canvas.height = t.height;
546
+ const e = this.canvas.getContext("2d", {
547
+ willReadFrequently: !0
548
+ });
549
+ if (!e)
550
+ throw new Error("Offscreen 2D context unavailable");
551
+ this.context = e, this.x = t.x || 0, this.y = t.y || 0, this.scale = t.scale ?? 1, this.rotation = t.rotation || 0, t.anchor && (this.anchor = t.anchor), this._redraw = t.redraw, this._redraw && this._redraw(this.context, this.canvas);
552
+ }
553
+ }
554
+ class L extends C {
555
+ #t = null;
556
+ /** 从图片源创建位图层(会把像素绘入离屏) */
557
+ static async fromImage(t) {
558
+ const e = await b(t.src, t.crossOrigin), s = t.width ?? e.naturalWidth, i = t.height ?? e.naturalHeight, n = new L({
559
+ name: t.name,
560
+ space: t.space ?? "world",
561
+ x: t.x ?? 0,
562
+ y: t.y ?? 0,
563
+ scale: t.scale ?? 1,
564
+ rotation: t.rotation ?? 0,
565
+ anchor: t.anchor ?? "topLeft",
566
+ width: s,
567
+ height: i
568
+ });
569
+ return n.context.clearRect(0, 0, n.canvas.width, n.canvas.height), n.context.drawImage(e, 0, 0, s, i), typeof t.src != "string" && (n.#t = e.src.startsWith("blob:") ? e.src : null), n;
570
+ }
571
+ /** 替换图源(尺寸会重配) */
572
+ async setSource(t, e) {
573
+ const s = await b(t, e);
574
+ if (this.canvas.width = s.naturalWidth, this.canvas.height = s.naturalHeight, this.context.setTransform(1, 0, 0, 1, 0, 0), this.context.clearRect(0, 0, this.canvas.width, this.canvas.height), this.context.drawImage(s, 0, 0), this.#t) {
575
+ try {
576
+ URL.revokeObjectURL(this.#t);
577
+ } catch {
578
+ }
579
+ this.#t = null;
580
+ }
581
+ typeof t != "string" && (this.#t = s.src.startsWith("blob:") ? s.src : null);
582
+ }
583
+ /** 在位图上作画(提供 ctx) */
584
+ paint(t) {
585
+ t(this.context, this.canvas);
586
+ }
587
+ /** 读取/写回像素 */
588
+ getImageData(t = 0, e = 0, s = this.canvas.width, i = this.canvas.height) {
589
+ return this.context.getImageData(t, e, s, i);
590
+ }
591
+ putImageData(t, e = 0, s = 0) {
592
+ this.context.putImageData(t, e, s);
593
+ }
594
+ /** 导出(PNG dataURL 或 ImageBitmap) */
595
+ toDataURL(t = "image/png", e) {
596
+ return this.canvas.toDataURL(t, e);
597
+ }
598
+ toImageBitmap(t) {
599
+ return createImageBitmap(this.canvas, t ?? {});
600
+ }
601
+ destroy() {
602
+ if (super.destroy?.(), this.#t) {
603
+ try {
604
+ URL.revokeObjectURL(this.#t);
605
+ } catch {
606
+ }
607
+ this.#t = null;
608
+ }
609
+ }
610
+ constructor(t) {
611
+ super({
612
+ name: t.name,
613
+ space: t.space ?? "world",
614
+ x: t.x,
615
+ y: t.y,
616
+ scale: t.scale,
617
+ rotation: t.rotation,
618
+ anchor: t.anchor ?? "topLeft",
619
+ width: t.width,
620
+ height: t.height
621
+ }), this.type = "bitmap";
622
+ }
623
+ }
624
+ class S {
625
+ _worldLayers = [];
626
+ _screenLayers = [];
627
+ /**
628
+ * Render all layers in target view.
629
+ */
630
+ _renderAllLayersIn(t, e) {
631
+ e.save(), t.applyWorldTransform(e);
632
+ for (const s of this._worldLayers)
633
+ !s.visible || s.opacity <= 0 || (e.save(), s.render(e, t), e.restore());
634
+ e.restore(), e.save(), t.applyScreenTransform(e);
635
+ for (const s of this._screenLayers)
636
+ !s.visible || s.opacity <= 0 || (e.save(), s.render(e, t), e.restore());
637
+ e.restore();
638
+ }
639
+ addLayer(t, e) {
640
+ const s = t.space === "world" ? this._worldLayers : this._screenLayers;
641
+ return typeof e == "number" && e >= 0 && e < s.length ? (s.splice(e, 0, t), t.id) : (s.push(t), t.id);
642
+ }
643
+ async createImageLayer(t) {
644
+ const e = await L.fromImage(t);
645
+ return this.addLayer(e), e;
646
+ }
647
+ createCanvasLayer(t) {
648
+ const e = new C(t);
649
+ return this.addLayer(e), e;
650
+ }
651
+ removeLayer(t) {
652
+ const e = this._worldLayers.findIndex((i) => i.id === t);
653
+ if (e >= 0) {
654
+ this._worldLayers[e].destroy?.(), this._worldLayers.splice(e, 1);
655
+ return;
656
+ }
657
+ const s = this._screenLayers.findIndex((i) => i.id === t);
658
+ s >= 0 && (this._screenLayers[s].destroy?.(), this._screenLayers.splice(s, 1));
659
+ }
660
+ /**
661
+ * Move a layer from one index to another.
662
+ * Both indices are relative to the layer's space (world or screen).
663
+ */
664
+ moveLayer(t, e) {
665
+ const s = this._worldLayers.findIndex((n) => n.id === t);
666
+ if (s >= 0) {
667
+ const [n] = this._worldLayers.splice(s, 1), a = Math.max(0, Math.min(e, this._worldLayers.length));
668
+ this._worldLayers.splice(a, 0, n);
669
+ return;
670
+ }
671
+ const i = this._screenLayers.findIndex((n) => n.id === t);
672
+ if (i >= 0) {
673
+ const [n] = this._screenLayers.splice(i, 1), a = Math.max(0, Math.min(e, this._screenLayers.length));
674
+ this._screenLayers.splice(a, 0, n);
675
+ }
676
+ }
677
+ getLayer(t) {
678
+ return this._worldLayers.find((e) => e.id === t) || this._screenLayers.find((e) => e.id === t);
679
+ }
680
+ getAllLayers(t) {
681
+ return t ? (t === "world" ? this._worldLayers : this._screenLayers).slice() : [...this._worldLayers, ...this._screenLayers];
682
+ }
683
+ /**
684
+ * Hit test all layers (top-first).
685
+ *
686
+ * @param x
687
+ * @param y
688
+ * @param space
689
+ */
690
+ hitTest(t, e, s = "world") {
691
+ const i = this.getAllLayers(s);
692
+ for (let n = i.length - 1; n >= 0; n--) {
693
+ const a = i[n];
694
+ if (a.hitTest && a.hitTest(t, e))
695
+ return a;
696
+ }
697
+ }
698
+ destroy() {
699
+ for (const t of [...this._worldLayers, ...this._screenLayers])
700
+ t.destroy?.();
701
+ this._worldLayers = [], this._screenLayers = [];
702
+ }
703
+ }
704
+ class I extends S {
705
+ // Composite cache for world layers
706
+ _compositeCache = null;
707
+ _compositeCacheCtx = null;
708
+ _compositeDirty = !0;
709
+ _lastCacheWidth = 0;
710
+ _lastCacheHeight = 0;
711
+ _cachedBoundsMinX = 0;
712
+ _cachedBoundsMinY = 0;
713
+ /**
714
+ * Mark the composite cache as dirty.
715
+ * Call this when any layer content changes.
716
+ */
717
+ markDirty() {
718
+ this._compositeDirty = !0;
719
+ }
720
+ /**
721
+ * Override addLayer to mark cache dirty.
722
+ */
723
+ addLayer(t, e) {
724
+ return this._compositeDirty = !0, super.addLayer(t, e);
725
+ }
726
+ /**
727
+ * Override removeLayer to mark cache dirty.
728
+ */
729
+ removeLayer(t) {
730
+ this._compositeDirty = !0, super.removeLayer(t);
731
+ }
732
+ /**
733
+ * Override moveLayer to mark cache dirty.
734
+ */
735
+ moveLayer(t, e) {
736
+ this._compositeDirty = !0, super.moveLayer(t, e);
737
+ }
738
+ /**
739
+ * Render all layers in target view.
740
+ * Uses composite cache for better performance with many layers.
741
+ */
742
+ renderAllLayersIn(t) {
743
+ const e = t.contentContext, s = this._worldLayers;
744
+ s.length !== 0 && (this._compositeDirty && this._rebuildCompositeCache(s), this._compositeCache && this._compositeCacheCtx && e.drawImage(this._compositeCache, this._cachedBoundsMinX, this._cachedBoundsMinY));
745
+ }
746
+ /**
747
+ * Rebuild the composite cache from all world layers.
748
+ */
749
+ _rebuildCompositeCache(t) {
750
+ let e = 0, s = 0, i = 0, n = 0, a = !1;
751
+ for (const c of t) {
752
+ const _ = c, m = _.canvas.width * _.scale, g = _.canvas.height * _.scale, f = _.anchor === "center" ? -m / 2 : 0, y = _.anchor === "center" ? -g / 2 : 0, v = _.x + f, l = _.y + y, u = v + m, p = l + g;
753
+ a ? (e = Math.min(e, v), s = Math.min(s, l), i = Math.max(i, u), n = Math.max(n, p)) : (e = v, s = l, i = u, n = p, a = !0);
754
+ }
755
+ if (!a) {
756
+ this._compositeDirty = !1;
757
+ return;
758
+ }
759
+ const h = Math.ceil(i - e), o = Math.ceil(n - s);
760
+ this._compositeCache || (this._compositeCache = document.createElement("canvas"), this._compositeCacheCtx = this._compositeCache.getContext("2d", { alpha: !0 })), (this._lastCacheWidth !== h || this._lastCacheHeight !== o) && (this._compositeCache.width = h, this._compositeCache.height = o, this._lastCacheWidth = h, this._lastCacheHeight = o), this._cachedBoundsMinX = e, this._cachedBoundsMinY = s;
761
+ const r = this._compositeCacheCtx;
762
+ r.clearRect(0, 0, h, o), r.save(), r.translate(-e, -s);
763
+ for (const c of t)
764
+ !c.visible || c.opacity <= 0 || (r.save(), c.render(r), r.restore());
765
+ r.restore(), this._compositeDirty = !1;
766
+ }
767
+ /**
768
+ * Override destroy to clean up cache.
769
+ */
770
+ destroy() {
771
+ super.destroy(), this._compositeCache = null, this._compositeCacheCtx = null;
772
+ }
773
+ }
774
+ class P extends S {
775
+ /**
776
+ * Render all layers in target view.
777
+ */
778
+ renderAllLayersIn(t) {
779
+ const e = t.topScreenContext;
780
+ this._renderAllLayersIn(t, e);
781
+ }
782
+ }
783
+ class T {
784
+ name = "interaction";
785
+ _view = null;
786
+ _options;
787
+ // Pan state
788
+ _panEnabled;
789
+ _zoomEnabled;
790
+ _dragging = !1;
791
+ _vx = 0;
792
+ // px/ms
793
+ _vy = 0;
794
+ _lastMoveTs = 0;
795
+ _activePointerId = null;
796
+ // Bound event handlers
797
+ _onDownBound = (t) => this._onPointerDown(t);
798
+ _onMoveBound = (t) => this._onPointerMove(t);
799
+ _onUpBound = () => this._onPointerUp();
800
+ _onWheelBound = (t) => this._onWheel(t);
801
+ constructor(t) {
802
+ this._options = {
803
+ panEnabled: !0,
804
+ zoomEnabled: !0,
805
+ friction: 0.92,
806
+ stopSpeed: 20 / 1e3,
807
+ emaAlpha: 0.25,
808
+ idleNoInertiaMs: 120,
809
+ wheelSensitivity: 15e-4,
810
+ ...t
811
+ }, this._panEnabled = this._options.panEnabled, this._zoomEnabled = this._options.zoomEnabled;
812
+ }
813
+ install(t) {
814
+ this._view = t;
815
+ const e = t.canvas;
816
+ e.addEventListener("wheel", this._onWheelBound, { passive: !1 }), e.addEventListener("pointerdown", this._onDownBound), window.addEventListener("pointermove", this._onMoveBound), window.addEventListener("pointerup", this._onUpBound), t.onUpdate(this._onUpdate);
817
+ }
818
+ destroy() {
819
+ if (!this._view)
820
+ return;
821
+ const t = this._view.canvas;
822
+ t.removeEventListener("wheel", this._onWheelBound), t.removeEventListener("pointerdown", this._onDownBound), window.removeEventListener("pointermove", this._onMoveBound), window.removeEventListener("pointerup", this._onUpBound), this._view.offUpdate(this._onUpdate), this._view = null;
823
+ }
824
+ // --- Public API ---
825
+ isPanEnabled() {
826
+ return this._panEnabled;
827
+ }
828
+ isZoomEnabled() {
829
+ return this._zoomEnabled;
830
+ }
831
+ setPanEnabled(t) {
832
+ this._panEnabled !== t && (this._panEnabled = t, t || (this._dragging = !1, this._vx = 0, this._vy = 0));
833
+ }
834
+ setZoomEnabled(t) {
835
+ this._zoomEnabled = t;
836
+ }
837
+ setWheelSensitivity(t) {
838
+ this._options.wheelSensitivity = t;
839
+ }
840
+ isDragging() {
841
+ return this._dragging;
842
+ }
843
+ // --- Internals ---
844
+ _onUpdate = (t) => {
845
+ if (!this._view)
846
+ return;
847
+ const { friction: e, stopSpeed: s } = this._options, i = Math.hypot(this._vx, this._vy) >= s;
848
+ if (!this._dragging && this._panEnabled && i) {
849
+ const n = this._vx * t, a = this._vy * t;
850
+ this._view.panBy(n, a), this._vx *= e, this._vy *= e, Math.hypot(this._vx, this._vy) < s && (this._vx = 0, this._vy = 0);
851
+ } else this._panEnabled || (this._vx = 0, this._vy = 0);
852
+ };
853
+ _onPointerDown(t) {
854
+ if (!(t.button !== 0 || !this._panEnabled)) {
855
+ this._dragging = !0, this._vx = 0, this._vy = 0, this._lastMoveTs = performance.now(), this._activePointerId = t.pointerId;
856
+ try {
857
+ this._view?.canvas.setPointerCapture(t.pointerId);
858
+ } catch {
859
+ }
860
+ }
861
+ }
862
+ _onPointerMove(t) {
863
+ if (!this._dragging || !this._panEnabled || !this._view)
864
+ return;
865
+ const e = performance.now(), s = Math.max(1, e - (this._lastMoveTs || e - 16));
866
+ this._lastMoveTs = e;
867
+ const i = t.movementX, n = t.movementY;
868
+ this._view.panBy(i, n);
869
+ const a = this._options.emaAlpha, h = i / s, o = n / s;
870
+ this._vx = (1 - a) * this._vx + a * h, this._vy = (1 - a) * this._vy + a * o;
871
+ }
872
+ _onPointerUp() {
873
+ if (!this._dragging)
874
+ return;
875
+ this._dragging = !1;
876
+ const t = performance.now(), e = this._lastMoveTs ? t - this._lastMoveTs : 1 / 0;
877
+ if (this._activePointerId != null && this._view) {
878
+ try {
879
+ this._view.canvas.releasePointerCapture(this._activePointerId);
880
+ } catch {
881
+ }
882
+ this._activePointerId = null;
883
+ }
884
+ if (e >= this._options.idleNoInertiaMs)
885
+ this._vx = 0, this._vy = 0;
886
+ else {
887
+ const s = Math.pow(this._options.friction, e / 16);
888
+ this._vx *= s, this._vy *= s;
889
+ }
890
+ Math.hypot(this._vx, this._vy) < this._options.stopSpeed && (this._vx = 0, this._vy = 0);
891
+ }
892
+ _getLineHeightPx() {
893
+ if (!this._view)
894
+ return 16;
895
+ const t = getComputedStyle(this._view.canvas).lineHeight;
896
+ if (!t || t === "normal")
897
+ return 16;
898
+ const e = parseFloat(t);
899
+ return Number.isFinite(e) ? e : 16;
900
+ }
901
+ _normalizeWheelDelta(t) {
902
+ if (!this._view)
903
+ return 0;
904
+ let e = t.deltaY;
905
+ if (t.deltaMode === 1)
906
+ e *= this._getLineHeightPx();
907
+ else if (t.deltaMode === 2) {
908
+ const s = this._view.canvas.clientHeight || window.innerHeight;
909
+ e *= s || 800;
910
+ }
911
+ return e;
912
+ }
913
+ _onWheel(t) {
914
+ if (!this._zoomEnabled || !this._view)
915
+ return;
916
+ t.preventDefault(), t.stopPropagation();
917
+ const e = this._normalizeWheelDelta(t), s = this._view.canvas.getBoundingClientRect(), i = t.clientX - s.left, n = t.clientY - s.top;
918
+ let a = -e * this._options.wheelSensitivity;
919
+ t.ctrlKey || t.metaKey ? a *= 1.6 : t.shiftKey && (a *= 0.6), this._view.zoomByLogAtScreen(i, n, a);
920
+ }
921
+ }
922
+ function E(d) {
923
+ return new T(d);
924
+ }
925
+ class k {
926
+ name = "document";
927
+ _view = null;
928
+ _options;
929
+ // Document rectangle (world coords)
930
+ _enabled = !1;
931
+ _x = 0;
932
+ _y = 0;
933
+ _width = 0;
934
+ _height = 0;
935
+ // Screen margins (CSS px)
936
+ _marginL = 0;
937
+ _marginR = 0;
938
+ _marginT = 0;
939
+ _marginB = 0;
940
+ _panClampMode = "minVisible";
941
+ constructor(t) {
942
+ this._options = {
943
+ rect: { x: 0, y: 0, width: 0, height: 0 },
944
+ margins: {},
945
+ drawBorder: !1,
946
+ minVisiblePx: 30,
947
+ panClampMode: "minVisible",
948
+ ...t
949
+ }, t?.rect && (this._enabled = !0, this._x = t.rect.x, this._y = t.rect.y, this._width = t.rect.width, this._height = t.rect.height), t?.margins && (this._marginL = t.margins.left ?? 0, this._marginR = t.margins.right ?? 0, this._marginT = t.margins.top ?? 0, this._marginB = t.margins.bottom ?? 0), this._panClampMode = this._options.panClampMode;
950
+ }
951
+ install(t) {
952
+ this._view = t, t.onUpdate(this._onUpdate), t.onBeforeRender(this._onBeforeRender), t.onAfterRender(this._onAfterRender);
953
+ }
954
+ destroy() {
955
+ this._view && (this._view.offUpdate(this._onUpdate), this._view.offBeforeRender(this._onBeforeRender), this._view.offAfterRender(this._onAfterRender), this._view = null);
956
+ }
957
+ // --- Public API ---
958
+ isEnabled() {
959
+ return this._enabled;
960
+ }
961
+ getRect() {
962
+ return {
963
+ x: this._x,
964
+ y: this._y,
965
+ width: this._width,
966
+ height: this._height
967
+ };
968
+ }
969
+ setRect(t, e, s, i) {
970
+ this._enabled = !0, this._x = t, this._y = e, this._width = s, this._height = i;
971
+ }
972
+ clearRect() {
973
+ this._enabled = !1;
974
+ }
975
+ setMargins(t) {
976
+ this._marginL = t.left ?? this._marginL, this._marginR = t.right ?? this._marginR, this._marginT = t.top ?? this._marginT, this._marginB = t.bottom ?? this._marginB;
977
+ }
978
+ getMargins() {
979
+ return {
980
+ left: this._marginL,
981
+ right: this._marginR,
982
+ top: this._marginT,
983
+ bottom: this._marginB
984
+ };
985
+ }
986
+ setPanClampMode(t) {
987
+ this._panClampMode = t;
988
+ }
989
+ getPanClampMode() {
990
+ return this._panClampMode;
991
+ }
992
+ /**
993
+ * Crop document and all world layers to the specified size.
994
+ */
995
+ cropTo(t) {
996
+ this._doResize("crop", t);
997
+ }
998
+ /**
999
+ * Resize document and all world layers to the specified size.
1000
+ */
1001
+ resizeTo(t) {
1002
+ this._doResize("resize", t);
1003
+ }
1004
+ /**
1005
+ * Zoom to fit the document in the viewport.
1006
+ */
1007
+ zoomToFit(t = "contain") {
1008
+ if (!this._enabled || !this._view)
1009
+ return;
1010
+ const e = this._view.dpr, s = this._view.canvas.width / e, i = this._view.canvas.height / e, n = Math.max(1, s - (this._marginL + this._marginR)), a = Math.max(1, i - (this._marginT + this._marginB));
1011
+ let h;
1012
+ const o = n / this._width, r = a / this._height;
1013
+ t === "contain" ? h = Math.min(o, r) : t === "cover" ? h = Math.max(o, r) : t === "fitWidth" ? h = o : h = r, h = Math.min(this._view.maxZoom, Math.max(this._view.minZoom, h));
1014
+ const c = this._marginL + (n - h * this._width) / 2, _ = this._marginT + (a - h * this._height) / 2, m = c - h * this._x, g = _ - h * this._y;
1015
+ this._view.setTransform(h, m, g);
1016
+ }
1017
+ /**
1018
+ * Check if a world point is inside the document.
1019
+ */
1020
+ isPointInDocument(t, e) {
1021
+ return this._enabled ? t >= this._x && t <= this._x + this._width && e >= this._y && e <= this._y + this._height : !0;
1022
+ }
1023
+ // --- Internals ---
1024
+ _onUpdate = () => {
1025
+ !this._enabled || !this._view || this._clampPan();
1026
+ };
1027
+ _onBeforeRender = (t) => {
1028
+ !this._enabled || !this._view || (t.save(), t.beginPath(), t.rect(this._x, this._y, this._width, this._height), t.clip());
1029
+ };
1030
+ _onAfterRender = (t) => {
1031
+ if (!(!this._enabled || !this._view) && (t.restore(), this._options.drawBorder)) {
1032
+ const e = this._view.zoom;
1033
+ t.save(), t.lineWidth = 1 / e, t.strokeStyle = "#cfcfcf", t.strokeRect(this._x, this._y, this._width, this._height), t.restore();
1034
+ }
1035
+ };
1036
+ _clampPan() {
1037
+ if (!this._view)
1038
+ return;
1039
+ const { zoom: t, tx: e, ty: s } = this._view.getTransform(), i = t, n = this._view.dpr, a = this._view.canvas.width / n, h = this._view.canvas.height / n, o = this._x, r = this._y, c = this._x + this._width, _ = this._y + this._height;
1040
+ let m = e, g = s;
1041
+ if (this._panClampMode === "margin") {
1042
+ const f = this._marginL - i * o, y = a - this._marginR - i * c, v = this._marginT - i * r, l = h - this._marginB - i * _, u = Math.max(1, a - (this._marginL + this._marginR)), p = Math.max(1, h - (this._marginT + this._marginB));
1043
+ i * this._width <= u ? m = this._marginL + (u - i * this._width) / 2 - i * this._x : m = Math.min(f, Math.max(y, e)), i * this._height <= p ? g = this._marginT + (p - i * this._height) / 2 - i * this._y : g = Math.min(v, Math.max(l, s));
1044
+ } else if (this._panClampMode === "minVisible") {
1045
+ const f = i * this._width, y = i * this._height, v = Math.min(this._options.minVisiblePx, f), l = Math.min(this._options.minVisiblePx, y), u = a - v - i * o, p = v - i * c, x = h - l - i * r, w = l - i * _;
1046
+ m = p <= u ? Math.min(u, Math.max(p, e)) : (p + u) / 2, g = w <= x ? Math.min(x, Math.max(w, s)) : (w + x) / 2;
1047
+ }
1048
+ (m !== e || g !== s) && this._view.setPan(m, g);
1049
+ }
1050
+ _doResize(t, e) {
1051
+ if (!this._view)
1052
+ return;
1053
+ const s = Math.max(1, Math.floor(e.width)), i = Math.max(1, Math.floor(e.height)), n = { width: s, height: i }, a = this._view.getLayerManagers();
1054
+ for (const h of a) {
1055
+ const o = h.getAllLayers("world");
1056
+ for (const r of o) {
1057
+ const c = r;
1058
+ typeof c.cropTo == "function" && typeof c.resizeTo == "function" && (t === "crop" ? c.cropTo(n) : c.resizeTo(n));
1059
+ }
1060
+ }
1061
+ this._width = s, this._height = i, this._enabled && this._clampPan();
1062
+ }
1063
+ }
1064
+ function A(d) {
1065
+ return new k(d);
1066
+ }
1067
+ class Z {
1068
+ type = "snapshot";
1069
+ _target;
1070
+ _beforeData;
1071
+ _afterData;
1072
+ _region;
1073
+ _isExecuted = !0;
1074
+ // Default to executed since we capture after the action
1075
+ /**
1076
+ * Create a snapshot command.
1077
+ *
1078
+ * @param target - The object to restore snapshots to (must implement ISnapshotable)
1079
+ * @param beforeData - The snapshot before the change
1080
+ * @param afterData - The snapshot after the change
1081
+ * @param options - Optional configuration
1082
+ */
1083
+ constructor(t, e, s, i) {
1084
+ this._target = t, this._beforeData = e, this._afterData = s, this._region = i?.region;
1085
+ }
1086
+ execute() {
1087
+ if (this._isExecuted)
1088
+ return;
1089
+ const t = this._region ? { x: this._region.x, y: this._region.y } : void 0;
1090
+ this._target.restoreSnapshot(this._afterData, t), this._isExecuted = !0;
1091
+ }
1092
+ undo() {
1093
+ if (!this._isExecuted)
1094
+ return;
1095
+ const t = this._region ? { x: this._region.x, y: this._region.y } : void 0;
1096
+ this._target.restoreSnapshot(this._beforeData, t), this._isExecuted = !1;
1097
+ }
1098
+ canMerge() {
1099
+ return !1;
1100
+ }
1101
+ merge() {
1102
+ return this;
1103
+ }
1104
+ }
1105
+ function D(d, t, e, s) {
1106
+ return !t || !e ? null : new Z(d, t, e, s);
1107
+ }
1108
+ class W {
1109
+ _maxHistorySize;
1110
+ _undoStack = [];
1111
+ _redoStack = [];
1112
+ /**
1113
+ * 执行命令并记录到历史
1114
+ * 用于需要执行+记录的场景(如从外部触发的命令)
1115
+ */
1116
+ executeCommand(t) {
1117
+ t.execute(), this.addCommand(t);
1118
+ }
1119
+ /**
1120
+ * 将已执行的命令添加到历史记录
1121
+ * 用于实时绘制场景(命令已经通过其他方式执行完成)
1122
+ */
1123
+ addCommand(t) {
1124
+ this._redoStack = [];
1125
+ const e = this._undoStack[this._undoStack.length - 1];
1126
+ if (e && e.canMerge?.(t) && e.merge) {
1127
+ const s = e.merge(t) ?? e;
1128
+ s !== e && (this._undoStack[this._undoStack.length - 1] = s);
1129
+ return;
1130
+ }
1131
+ this._undoStack.push(t), this._undoStack.length > this._maxHistorySize && this._undoStack.shift();
1132
+ }
1133
+ /**
1134
+ * 撤销操作
1135
+ */
1136
+ undo() {
1137
+ if (this._undoStack.length === 0)
1138
+ return null;
1139
+ const t = this._undoStack.pop();
1140
+ return t.undo(), this._redoStack.push(t), t;
1141
+ }
1142
+ /**
1143
+ * 重做操作
1144
+ */
1145
+ redo() {
1146
+ if (this._redoStack.length === 0)
1147
+ return null;
1148
+ const t = this._redoStack.pop();
1149
+ return t.execute(), this._undoStack.push(t), t;
1150
+ }
1151
+ /**
1152
+ * 检查是否可以撤销
1153
+ */
1154
+ canUndo() {
1155
+ return this._undoStack.length > 0;
1156
+ }
1157
+ /**
1158
+ * 检查是否可以重做
1159
+ */
1160
+ canRedo() {
1161
+ return this._redoStack.length > 0;
1162
+ }
1163
+ /**
1164
+ * 清空所有历史
1165
+ */
1166
+ clear() {
1167
+ this._undoStack = [], this._redoStack = [];
1168
+ }
1169
+ /**
1170
+ * 设置最大历史大小
1171
+ */
1172
+ setMaxHistorySize(t) {
1173
+ this._maxHistorySize = Math.max(1, t), this._undoStack.length > this._maxHistorySize && (this._undoStack = this._undoStack.slice(-this._maxHistorySize));
1174
+ }
1175
+ constructor(t) {
1176
+ this._maxHistorySize = t?.maxHistorySize ?? 50, this._undoStack = t?.undoStack ?? [], this._redoStack = t?.redoStack ?? [];
1177
+ }
1178
+ }
1179
+ export {
1180
+ L as BitmapLayer,
1181
+ C as CanvasLayer,
1182
+ I as ContentLayerManager,
1183
+ k as DocumentPlugin,
1184
+ W as HistoryManager,
1185
+ T as InteractionPlugin,
1186
+ z as LayerBase,
1187
+ S as LayerManagerBase,
1188
+ Z as SnapshotCommand,
1189
+ P as TopScreenLayerManager,
1190
+ B as ViewManager,
1191
+ A as createDocumentPlugin,
1192
+ E as createInteractionPlugin,
1193
+ D as createSnapshotCommand
1194
+ };