@gradio/imageeditor 0.14.4 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/InteractiveImageEditor.svelte +4 -7
- package/dist/InteractiveImageEditor.svelte +4 -3
- package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
- package/dist/shared/Controls.svelte +32 -1
- package/dist/shared/Controls.svelte.d.ts +2 -0
- package/dist/shared/ImageEditor.svelte +23 -1
- package/dist/shared/ImageEditor.svelte.d.ts +2 -1
- package/dist/shared/brush/brush-cursor.js +0 -42
- package/dist/shared/brush/brush-textures.d.ts +19 -2
- package/dist/shared/brush/brush-textures.js +120 -82
- package/dist/shared/brush/brush.js +0 -52
- package/dist/shared/{utils → core}/commands.d.ts +19 -58
- package/dist/shared/core/commands.js +70 -0
- package/dist/shared/core/editor.d.ts +4 -91
- package/dist/shared/core/editor.js +18 -708
- package/dist/shared/core/layers.d.ts +143 -0
- package/dist/shared/core/layers.js +774 -0
- package/dist/shared/image/image.d.ts +14 -4
- package/dist/shared/image/image.js +91 -18
- package/dist/shared/resize/resize.d.ts +31 -0
- package/dist/shared/resize/resize.js +127 -290
- package/dist/shared/zoom/zoom.js +0 -51
- package/package.json +7 -7
- package/shared/Controls.svelte +33 -1
- package/shared/ImageEditor.svelte +25 -9
- package/shared/Toolbar.svelte +1 -4
- package/shared/brush/ColorSwatch.svelte +0 -3
- package/shared/brush/brush-cursor.ts +0 -42
- package/shared/brush/brush-textures.ts +166 -83
- package/shared/brush/brush.ts +0 -53
- package/shared/core/commands.ts +118 -0
- package/shared/core/editor.ts +33 -957
- package/shared/core/layers.ts +1068 -0
- package/shared/image/image.ts +139 -21
- package/shared/resize/resize.ts +160 -299
- package/shared/zoom/zoom.ts +0 -51
- package/dist/shared/utils/commands.js +0 -73
- package/shared/utils/commands.ts +0 -174
package/shared/zoom/zoom.ts
CHANGED
|
@@ -17,11 +17,9 @@ export class ZoomTool implements Tool {
|
|
|
17
17
|
|
|
18
18
|
private image_editor_context!: ImageEditorContext;
|
|
19
19
|
|
|
20
|
-
// Constants
|
|
21
20
|
private readonly max_zoom = 10;
|
|
22
21
|
private readonly border_padding = 30;
|
|
23
22
|
|
|
24
|
-
// State
|
|
25
23
|
private pad_bottom = 0;
|
|
26
24
|
private is_pinching = false;
|
|
27
25
|
private is_dragging = false;
|
|
@@ -65,37 +63,30 @@ export class ZoomTool implements Tool {
|
|
|
65
63
|
* @returns {void}
|
|
66
64
|
*/
|
|
67
65
|
set_zoom(zoom_level: number | "fit"): void {
|
|
68
|
-
// Calculate the min zoom level
|
|
69
66
|
const fit_zoom = this.calculate_min_zoom(
|
|
70
67
|
this.local_dimensions.width,
|
|
71
68
|
this.local_dimensions.height
|
|
72
69
|
);
|
|
73
70
|
|
|
74
|
-
// Determine the target zoom level
|
|
75
71
|
let target_zoom: number;
|
|
76
72
|
const is_fit_zoom = zoom_level === "fit";
|
|
77
73
|
if (is_fit_zoom) {
|
|
78
74
|
target_zoom = fit_zoom;
|
|
79
75
|
} else {
|
|
80
|
-
// Ensure zoom_level is between 0 and 1, then scale it to the range [min_zoom, max_zoom]
|
|
81
76
|
target_zoom = Math.max(0, Math.min(this.max_zoom, zoom_level));
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
// Get the canvas dimensions
|
|
85
79
|
const canvas = this.image_editor_context.app.screen;
|
|
86
80
|
const canvas_width = canvas.width;
|
|
87
81
|
const canvas_height = canvas.height;
|
|
88
82
|
|
|
89
|
-
// Determine the center point for zooming
|
|
90
83
|
let center_point: { x: number; y: number };
|
|
91
84
|
|
|
92
|
-
// Always use the center of the screen
|
|
93
85
|
center_point = {
|
|
94
86
|
x: canvas_width / 2,
|
|
95
87
|
y: canvas_height / 2
|
|
96
88
|
};
|
|
97
89
|
|
|
98
|
-
// Apply the zoom using the existing zoom_to_point method
|
|
99
90
|
this.zoom_to_point(target_zoom, center_point, true, is_fit_zoom);
|
|
100
91
|
}
|
|
101
92
|
|
|
@@ -115,22 +106,17 @@ export class ZoomTool implements Tool {
|
|
|
115
106
|
this.current_tool = tool;
|
|
116
107
|
this.current_subtool = subtool;
|
|
117
108
|
this.pad_bottom = context.pad_bottom;
|
|
118
|
-
// Initialize zoom and position
|
|
119
109
|
const { width, height } = await this.get_container_dimensions();
|
|
120
110
|
const fit_zoom = this.calculate_min_zoom(width, height);
|
|
121
|
-
// Use fit zoom or 1, whichever is smaller
|
|
122
111
|
const min_zoom = Math.min(fit_zoom, 1);
|
|
123
112
|
|
|
124
|
-
// Set initial zoom
|
|
125
113
|
this.local_scale = min_zoom;
|
|
126
114
|
|
|
127
|
-
// Use simple center positioning
|
|
128
115
|
const canvas = this.image_editor_context.app.screen;
|
|
129
116
|
|
|
130
117
|
const center_x = canvas.width / 2;
|
|
131
118
|
const center_y = canvas.height / 2;
|
|
132
119
|
|
|
133
|
-
// Calculate initial position to center the scaled image
|
|
134
120
|
const scaled_width = width * min_zoom;
|
|
135
121
|
const scaled_height = height * min_zoom;
|
|
136
122
|
const initial_x = center_x - scaled_width / 2;
|
|
@@ -138,10 +124,8 @@ export class ZoomTool implements Tool {
|
|
|
138
124
|
|
|
139
125
|
this.local_position = { x: initial_x, y: initial_y };
|
|
140
126
|
|
|
141
|
-
// Setup event listeners
|
|
142
127
|
this.setup_event_listeners();
|
|
143
128
|
|
|
144
|
-
// Update context with initial values
|
|
145
129
|
await this.image_editor_context.set_image_properties({
|
|
146
130
|
scale: min_zoom,
|
|
147
131
|
position: { x: initial_x, y: initial_y }
|
|
@@ -170,15 +154,12 @@ export class ZoomTool implements Tool {
|
|
|
170
154
|
|
|
171
155
|
canvas.addEventListener("wheel", this.prevent_default, { passive: false });
|
|
172
156
|
|
|
173
|
-
// Configure stage for interaction
|
|
174
157
|
stage.eventMode = "static";
|
|
175
158
|
stage.hitArea = this.image_editor_context.app.screen;
|
|
176
159
|
|
|
177
|
-
// Add wheel event with options
|
|
178
160
|
const wheel_listener = this.handle_wheel.bind(this);
|
|
179
161
|
stage.addEventListener("wheel", wheel_listener, { passive: false });
|
|
180
162
|
|
|
181
|
-
// Setup touch/pointer events based on device
|
|
182
163
|
if ("ontouchstart" in window) {
|
|
183
164
|
stage.addEventListener(
|
|
184
165
|
"touchstart",
|
|
@@ -219,7 +200,6 @@ export class ZoomTool implements Tool {
|
|
|
219
200
|
const scroll_speed = is_trackpad ? 30 : 10;
|
|
220
201
|
|
|
221
202
|
if (event.altKey || event.metaKey) {
|
|
222
|
-
// Handle zooming with our new simplified logic
|
|
223
203
|
const local_point = this.image_editor_context.app.stage.toLocal(
|
|
224
204
|
event.global
|
|
225
205
|
);
|
|
@@ -227,7 +207,6 @@ export class ZoomTool implements Tool {
|
|
|
227
207
|
const new_scale = this.local_scale * (1 + zoom_delta);
|
|
228
208
|
this.zoom_to_point(new_scale, local_point, true);
|
|
229
209
|
} else {
|
|
230
|
-
// Handle panning with bounds
|
|
231
210
|
const delta_x = event.deltaX;
|
|
232
211
|
const delta_y = event.deltaY;
|
|
233
212
|
const raw_position = {
|
|
@@ -244,11 +223,9 @@ export class ZoomTool implements Tool {
|
|
|
244
223
|
|
|
245
224
|
let final_position = { ...raw_position };
|
|
246
225
|
|
|
247
|
-
// If image is smaller than available space, center it
|
|
248
226
|
if (scaled_width <= available_width) {
|
|
249
227
|
final_position.x = (canvas.width - scaled_width) / 2;
|
|
250
228
|
} else {
|
|
251
|
-
// Apply 50% rule for horizontal bounds
|
|
252
229
|
const max_offset = canvas.width / 2;
|
|
253
230
|
const left_bound = max_offset;
|
|
254
231
|
const right_bound = canvas.width - max_offset - scaled_width;
|
|
@@ -262,7 +239,6 @@ export class ZoomTool implements Tool {
|
|
|
262
239
|
final_position.y =
|
|
263
240
|
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
264
241
|
} else {
|
|
265
|
-
// Apply 50% rule for vertical bounds, accounting for pad_bottom
|
|
266
242
|
const max_offset = (canvas.height - this.pad_bottom) / 2;
|
|
267
243
|
const top_bound = max_offset;
|
|
268
244
|
const bottom_bound =
|
|
@@ -358,17 +334,13 @@ export class ZoomTool implements Tool {
|
|
|
358
334
|
return 1;
|
|
359
335
|
}
|
|
360
336
|
|
|
361
|
-
// Calculate available space accounting for padding
|
|
362
337
|
const available_width = viewport_width - this.border_padding * 2;
|
|
363
338
|
const available_height =
|
|
364
339
|
viewport_height - this.border_padding * 2 - this.pad_bottom;
|
|
365
340
|
|
|
366
|
-
// Calculate zoom ratios for both dimensions
|
|
367
341
|
const width_ratio = available_width / container_width;
|
|
368
342
|
const height_ratio = available_height / container_height;
|
|
369
343
|
|
|
370
|
-
// Use the smaller ratio to ensure the image fits in both dimensions
|
|
371
|
-
// Note: we no longer cap at 1, as 'fit' can be above 1
|
|
372
344
|
return Math.min(width_ratio, height_ratio);
|
|
373
345
|
}
|
|
374
346
|
|
|
@@ -380,7 +352,6 @@ export class ZoomTool implements Tool {
|
|
|
380
352
|
private handle_touch_start(event: FederatedPointerEvent): void {
|
|
381
353
|
event.preventDefault();
|
|
382
354
|
|
|
383
|
-
// Cast to any to access TouchEvent properties
|
|
384
355
|
const touchEvent = event as any;
|
|
385
356
|
|
|
386
357
|
if (touchEvent.touches && touchEvent.touches.length === 2) {
|
|
@@ -407,7 +378,6 @@ export class ZoomTool implements Tool {
|
|
|
407
378
|
private handle_touch_move(event: FederatedPointerEvent): void {
|
|
408
379
|
event.preventDefault();
|
|
409
380
|
|
|
410
|
-
// Cast to any to access TouchEvent properties
|
|
411
381
|
const touchEvent = event as any;
|
|
412
382
|
|
|
413
383
|
if (
|
|
@@ -433,7 +403,6 @@ export class ZoomTool implements Tool {
|
|
|
433
403
|
const scale_val = current_distance / this.last_pinch_distance;
|
|
434
404
|
this.last_pinch_distance = current_distance;
|
|
435
405
|
|
|
436
|
-
// Update zoom based on pinch
|
|
437
406
|
this.zoom_to_point(this.local_scale * scale_val, pinch_center);
|
|
438
407
|
} else if (
|
|
439
408
|
this.is_dragging &&
|
|
@@ -450,7 +419,6 @@ export class ZoomTool implements Tool {
|
|
|
450
419
|
const dx = current_position.x - this.last_touch_position.x;
|
|
451
420
|
const dy = current_position.y - this.last_touch_position.y;
|
|
452
421
|
|
|
453
|
-
// Update position based on drag
|
|
454
422
|
this.image_editor_context.set_image_properties({
|
|
455
423
|
position: {
|
|
456
424
|
x: this.local_position.x + dx,
|
|
@@ -470,7 +438,6 @@ export class ZoomTool implements Tool {
|
|
|
470
438
|
private handle_touch_end(event: FederatedPointerEvent): void {
|
|
471
439
|
event.preventDefault();
|
|
472
440
|
|
|
473
|
-
// Cast to any to access TouchEvent properties
|
|
474
441
|
const touchEvent = event as any;
|
|
475
442
|
|
|
476
443
|
if (touchEvent.touches && touchEvent.touches.length < 2) {
|
|
@@ -507,7 +474,6 @@ export class ZoomTool implements Tool {
|
|
|
507
474
|
const scaled_width = this.local_dimensions.width * this.local_scale;
|
|
508
475
|
const scaled_height = this.local_dimensions.height * this.local_scale;
|
|
509
476
|
|
|
510
|
-
// Calculate center position, accounting for pad_bottom
|
|
511
477
|
const center_position = {
|
|
512
478
|
x: (canvas.width - scaled_width) / 2,
|
|
513
479
|
y: (canvas.height - scaled_height - this.pad_bottom) / 2
|
|
@@ -555,43 +521,35 @@ export class ZoomTool implements Tool {
|
|
|
555
521
|
hard?: boolean,
|
|
556
522
|
is_fit_zoom?: boolean
|
|
557
523
|
): void {
|
|
558
|
-
// Get the cursor position relative to the image's top-left corner
|
|
559
524
|
const cursor_relative_to_image = {
|
|
560
525
|
x: (point.x - this.local_position.x) / this.local_scale,
|
|
561
526
|
y: (point.y - this.local_position.y) / this.local_scale
|
|
562
527
|
};
|
|
563
528
|
|
|
564
|
-
// Calculate what percentage of the image width/height this represents
|
|
565
529
|
const image_percentages = {
|
|
566
530
|
x: cursor_relative_to_image.x / this.local_dimensions.width,
|
|
567
531
|
y: cursor_relative_to_image.y / this.local_dimensions.height
|
|
568
532
|
};
|
|
569
533
|
|
|
570
|
-
// Apply zoom limits
|
|
571
534
|
const fit_zoom = this.calculate_min_zoom(
|
|
572
535
|
this.local_dimensions.width,
|
|
573
536
|
this.local_dimensions.height
|
|
574
537
|
);
|
|
575
|
-
// Use the fit zoom or 1, whichever is smaller as the minimum zoom
|
|
576
538
|
const min_zoom = Math.min(fit_zoom, 1);
|
|
577
539
|
new_zoom = Math.min(Math.max(new_zoom, min_zoom), this.max_zoom);
|
|
578
540
|
|
|
579
|
-
// Calculate new dimensions
|
|
580
541
|
const new_scaled_width = this.local_dimensions.width * new_zoom;
|
|
581
542
|
const new_scaled_height = this.local_dimensions.height * new_zoom;
|
|
582
543
|
|
|
583
|
-
// Calculate new position maintaining the same relative cursor position
|
|
584
544
|
let new_position = {
|
|
585
545
|
x: point.x - new_scaled_width * image_percentages.x,
|
|
586
546
|
y: point.y - new_scaled_height * image_percentages.y
|
|
587
547
|
};
|
|
588
548
|
|
|
589
|
-
// Center the image if we're at minimum zoom or if this is a fit zoom operation
|
|
590
549
|
if (new_zoom === min_zoom || is_fit_zoom) {
|
|
591
550
|
const canvas_width = this.image_editor_context.app.screen.width;
|
|
592
551
|
const canvas_height = this.image_editor_context.app.screen.height;
|
|
593
552
|
|
|
594
|
-
// Center the image, accounting for pad_bottom
|
|
595
553
|
new_position = {
|
|
596
554
|
x: (canvas_width - new_scaled_width) / 2,
|
|
597
555
|
y: (canvas_height - this.pad_bottom - new_scaled_height) / 2
|
|
@@ -612,7 +570,6 @@ export class ZoomTool implements Tool {
|
|
|
612
570
|
* @private
|
|
613
571
|
*/
|
|
614
572
|
private handle_pointer_down(event: FederatedPointerEvent): void {
|
|
615
|
-
// Only allow dragging if the current tool is 'pan'
|
|
616
573
|
if (event.button === 0 && this.current_tool === "pan") {
|
|
617
574
|
this.is_pointer_dragging = true;
|
|
618
575
|
this.drag_start.copyFrom(event.global);
|
|
@@ -627,7 +584,6 @@ export class ZoomTool implements Tool {
|
|
|
627
584
|
* @private
|
|
628
585
|
*/
|
|
629
586
|
private handle_pointer_move(event: FederatedPointerEvent): void {
|
|
630
|
-
// Only handle move if we're dragging and the tool is 'pan'
|
|
631
587
|
if (this.is_pointer_dragging && this.current_tool === "pan") {
|
|
632
588
|
const raw_position = {
|
|
633
589
|
x: event.global.x - this.drag_start.x,
|
|
@@ -643,11 +599,9 @@ export class ZoomTool implements Tool {
|
|
|
643
599
|
|
|
644
600
|
let final_position = { ...raw_position };
|
|
645
601
|
|
|
646
|
-
// If image is smaller than available space, center it
|
|
647
602
|
if (scaled_width <= available_width) {
|
|
648
603
|
final_position.x = (canvas.width - scaled_width) / 2;
|
|
649
604
|
} else {
|
|
650
|
-
// Allow free movement during drag, bounds will be applied on pointer up
|
|
651
605
|
final_position.x = raw_position.x;
|
|
652
606
|
}
|
|
653
607
|
|
|
@@ -655,7 +609,6 @@ export class ZoomTool implements Tool {
|
|
|
655
609
|
final_position.y =
|
|
656
610
|
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
657
611
|
} else {
|
|
658
|
-
// Allow free movement during drag, bounds will be applied on pointer up
|
|
659
612
|
final_position.y = raw_position.y;
|
|
660
613
|
}
|
|
661
614
|
|
|
@@ -672,7 +625,6 @@ export class ZoomTool implements Tool {
|
|
|
672
625
|
* @private
|
|
673
626
|
*/
|
|
674
627
|
private handle_pointer_up(event: FederatedPointerEvent): void {
|
|
675
|
-
// Only handle up if we're dragging and the tool is 'pan'
|
|
676
628
|
if (this.is_pointer_dragging && this.current_tool === "pan") {
|
|
677
629
|
this.is_pointer_dragging = false;
|
|
678
630
|
|
|
@@ -690,11 +642,9 @@ export class ZoomTool implements Tool {
|
|
|
690
642
|
|
|
691
643
|
let final_position = { ...raw_position };
|
|
692
644
|
|
|
693
|
-
// If image is smaller than available space, center it
|
|
694
645
|
if (scaled_width <= available_width) {
|
|
695
646
|
final_position.x = (canvas.width - scaled_width) / 2;
|
|
696
647
|
} else {
|
|
697
|
-
// Apply 50% rule for horizontal snapping
|
|
698
648
|
const max_offset = canvas.width / 2;
|
|
699
649
|
const left_bound = max_offset;
|
|
700
650
|
const right_bound = canvas.width - max_offset - scaled_width;
|
|
@@ -708,7 +658,6 @@ export class ZoomTool implements Tool {
|
|
|
708
658
|
final_position.y =
|
|
709
659
|
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
710
660
|
} else {
|
|
711
|
-
// Apply 50% rule for vertical snapping, accounting for pad_bottom
|
|
712
661
|
const max_offset = (canvas.height - this.pad_bottom) / 2;
|
|
713
662
|
const top_bound = max_offset;
|
|
714
663
|
const bottom_bound =
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { writable } from "svelte/store";
|
|
2
|
-
/**
|
|
3
|
-
* Creates a command node
|
|
4
|
-
* @param command command to add to the node
|
|
5
|
-
* @returns a command node
|
|
6
|
-
*/
|
|
7
|
-
function command_node(command) {
|
|
8
|
-
return {
|
|
9
|
-
command: command || null,
|
|
10
|
-
next: null,
|
|
11
|
-
previous: null,
|
|
12
|
-
push: function (command) {
|
|
13
|
-
const node = command_node(command);
|
|
14
|
-
node.previous = this;
|
|
15
|
-
this.next = node;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Creates a command manager
|
|
21
|
-
* @returns a command manager
|
|
22
|
-
*/
|
|
23
|
-
export function command_manager() {
|
|
24
|
-
let history = command_node();
|
|
25
|
-
const can_undo = writable(false);
|
|
26
|
-
const can_redo = writable(false);
|
|
27
|
-
const current_history = writable(history);
|
|
28
|
-
return {
|
|
29
|
-
undo: function () {
|
|
30
|
-
if (history.previous) {
|
|
31
|
-
history.command?.undo();
|
|
32
|
-
history = history.previous;
|
|
33
|
-
}
|
|
34
|
-
can_undo.set(!!history.previous);
|
|
35
|
-
can_redo.set(!!history.next);
|
|
36
|
-
current_history.set(history);
|
|
37
|
-
},
|
|
38
|
-
redo: function () {
|
|
39
|
-
if (history.next) {
|
|
40
|
-
history.next.command?.execute();
|
|
41
|
-
history = history.next;
|
|
42
|
-
}
|
|
43
|
-
can_undo.set(!!history.previous);
|
|
44
|
-
can_redo.set(!!history.next);
|
|
45
|
-
current_history.set(history);
|
|
46
|
-
},
|
|
47
|
-
execute: function (command) {
|
|
48
|
-
command.execute();
|
|
49
|
-
history.push(command);
|
|
50
|
-
history = history.next;
|
|
51
|
-
can_undo.set(!!history.previous);
|
|
52
|
-
can_redo.set(!!history.next);
|
|
53
|
-
current_history.set(history);
|
|
54
|
-
},
|
|
55
|
-
hydrate: function (full_history) {
|
|
56
|
-
setTimeout(() => {
|
|
57
|
-
while (full_history.next) {
|
|
58
|
-
this.execute(full_history.next.command);
|
|
59
|
-
full_history = full_history.next;
|
|
60
|
-
}
|
|
61
|
-
}, 1000);
|
|
62
|
-
},
|
|
63
|
-
can_undo,
|
|
64
|
-
can_redo,
|
|
65
|
-
current_history,
|
|
66
|
-
reset: function () {
|
|
67
|
-
history = command_node();
|
|
68
|
-
can_undo.set(false);
|
|
69
|
-
can_redo.set(false);
|
|
70
|
-
current_history.set(history);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
package/shared/utils/commands.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { writable, type Writable } from "svelte/store";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Base command interface that is added to the command_managers history
|
|
5
|
-
*/
|
|
6
|
-
export interface Command {
|
|
7
|
-
/**
|
|
8
|
-
* Optionally called when the command is first executed for multi-step commands
|
|
9
|
-
* @param args arguments to pass to the command
|
|
10
|
-
*/
|
|
11
|
-
start?: (...args: any) => any | Promise<any>;
|
|
12
|
-
/**
|
|
13
|
-
* Optionally called when the command is continued for multi-step commands
|
|
14
|
-
* @param args arguments to pass to the command
|
|
15
|
-
*/
|
|
16
|
-
continue?: (...args: any) => any | Promise<any>;
|
|
17
|
-
/**
|
|
18
|
-
* Optionally called when the command is stopped for multi-step commands
|
|
19
|
-
* @param args arguments to pass to the command
|
|
20
|
-
*/
|
|
21
|
-
stop?: (...args: any) => any | Promise<any>;
|
|
22
|
-
/**
|
|
23
|
-
* Called by the command manager to execute the command, can act as a no-op if the work has already been done
|
|
24
|
-
* This function must be able to recreate the command if the command is undone and redone (`stop`/`start`/`continue` will not be called again)
|
|
25
|
-
*/
|
|
26
|
-
execute(): any | Promise<any>;
|
|
27
|
-
/**
|
|
28
|
-
* Called by the command manager to undo the command
|
|
29
|
-
* This function must be able to undo the work done by the execute function
|
|
30
|
-
*/
|
|
31
|
-
undo(): any | Promise<any>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Command manager interface that handles the undo/redo history
|
|
36
|
-
*/
|
|
37
|
-
export interface CommandManager {
|
|
38
|
-
/**
|
|
39
|
-
* Undo the last command
|
|
40
|
-
*/
|
|
41
|
-
undo(): void;
|
|
42
|
-
/**
|
|
43
|
-
* Redo the last undone command
|
|
44
|
-
*/
|
|
45
|
-
redo(): void;
|
|
46
|
-
/**
|
|
47
|
-
* Execute a command and add it to the history
|
|
48
|
-
* @param command command to execute
|
|
49
|
-
*/
|
|
50
|
-
execute(command: Command): void;
|
|
51
|
-
/**
|
|
52
|
-
* Whether or not there are commands that can be undone
|
|
53
|
-
* Observable store that you can subscribe to for updates
|
|
54
|
-
*/
|
|
55
|
-
readonly can_undo: Writable<boolean>;
|
|
56
|
-
/**
|
|
57
|
-
* Whether or not there are commands that can be redone
|
|
58
|
-
* Observable store that you can subscribe to for updates
|
|
59
|
-
*/
|
|
60
|
-
readonly can_redo: Writable<boolean>;
|
|
61
|
-
/**
|
|
62
|
-
* Resets the history
|
|
63
|
-
*/
|
|
64
|
-
reset(): void;
|
|
65
|
-
/**
|
|
66
|
-
* The current history node
|
|
67
|
-
* Observable store that you can subscribe to for updates
|
|
68
|
-
*/
|
|
69
|
-
readonly current_history: Writable<CommandNode>;
|
|
70
|
-
/**
|
|
71
|
-
* Hydrates the command manager with a full history
|
|
72
|
-
* @param full_history the full history to hydrate with
|
|
73
|
-
*/
|
|
74
|
-
hydrate(full_history: CommandNode): void;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Command node interface that is used to create the undo/redo history
|
|
79
|
-
*/
|
|
80
|
-
export interface CommandNode {
|
|
81
|
-
/**
|
|
82
|
-
* Command that the node holds
|
|
83
|
-
*/
|
|
84
|
-
command: Command | null;
|
|
85
|
-
/**
|
|
86
|
-
* Next command in the history
|
|
87
|
-
*/
|
|
88
|
-
next: CommandNode | null;
|
|
89
|
-
/**
|
|
90
|
-
* Previous command in the history
|
|
91
|
-
*/
|
|
92
|
-
previous: CommandNode | null;
|
|
93
|
-
/**
|
|
94
|
-
* Push a command onto the history
|
|
95
|
-
* @param command command to push onto the history
|
|
96
|
-
*/
|
|
97
|
-
push(command: Command): void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Creates a command node
|
|
102
|
-
* @param command command to add to the node
|
|
103
|
-
* @returns a command node
|
|
104
|
-
*/
|
|
105
|
-
function command_node(command?: Command): CommandNode {
|
|
106
|
-
return {
|
|
107
|
-
command: command || null,
|
|
108
|
-
next: null,
|
|
109
|
-
previous: null,
|
|
110
|
-
push: function (command: Command) {
|
|
111
|
-
const node = command_node(command);
|
|
112
|
-
node.previous = this;
|
|
113
|
-
this.next = node;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Creates a command manager
|
|
120
|
-
* @returns a command manager
|
|
121
|
-
*/
|
|
122
|
-
export function command_manager(): CommandManager {
|
|
123
|
-
let history: CommandNode = command_node();
|
|
124
|
-
const can_undo = writable(false);
|
|
125
|
-
const can_redo = writable(false);
|
|
126
|
-
const current_history = writable(history);
|
|
127
|
-
return {
|
|
128
|
-
undo: function () {
|
|
129
|
-
if (history.previous) {
|
|
130
|
-
history.command?.undo();
|
|
131
|
-
history = history.previous;
|
|
132
|
-
}
|
|
133
|
-
can_undo.set(!!history.previous);
|
|
134
|
-
can_redo.set(!!history.next);
|
|
135
|
-
current_history.set(history);
|
|
136
|
-
},
|
|
137
|
-
redo: function () {
|
|
138
|
-
if (history.next) {
|
|
139
|
-
history.next.command?.execute();
|
|
140
|
-
history = history.next;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
can_undo.set(!!history.previous);
|
|
144
|
-
can_redo.set(!!history.next);
|
|
145
|
-
current_history.set(history);
|
|
146
|
-
},
|
|
147
|
-
execute: function (command: Command) {
|
|
148
|
-
command.execute();
|
|
149
|
-
history.push(command);
|
|
150
|
-
history = history.next!;
|
|
151
|
-
can_undo.set(!!history.previous);
|
|
152
|
-
can_redo.set(!!history.next);
|
|
153
|
-
current_history.set(history);
|
|
154
|
-
},
|
|
155
|
-
hydrate: function (full_history: CommandNode) {
|
|
156
|
-
setTimeout(() => {
|
|
157
|
-
while (full_history.next) {
|
|
158
|
-
this.execute(full_history.next.command!);
|
|
159
|
-
full_history = full_history.next;
|
|
160
|
-
}
|
|
161
|
-
}, 1000);
|
|
162
|
-
},
|
|
163
|
-
can_undo,
|
|
164
|
-
can_redo,
|
|
165
|
-
current_history,
|
|
166
|
-
|
|
167
|
-
reset: function () {
|
|
168
|
-
history = command_node();
|
|
169
|
-
can_undo.set(false);
|
|
170
|
-
can_redo.set(false);
|
|
171
|
-
current_history.set(history);
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|