Skip to main content

egui_charts/drawings/services/
history.rs

1//! History Service - undo/redo for drawing operations
2//!
3//! Implements command pattern for reversible drawing operations.
4//!
5//! # Commands
6//!
7//! - `Add` - A new drawing was created
8//! - `Delete` - A drawing was deleted
9//! - `Modify` - A drawing was modified (move, resize, style change)
10//! - `Reorder` - Several drawing layers changed in one move
11//!
12//! # Usage
13//!
14//! ```
15//! use egui_charts::drawings::services::HistoryService;
16//! use egui_charts::drawings::{Drawing, DrawingToolType};
17//!
18//! let mut drawings = Vec::new();
19//! let mut history = HistoryService::new();
20//!
21//! // After creating a drawing
22//! let drawing = Drawing::new(1, DrawingToolType::TrendLine);
23//! drawings.push(drawing.clone());
24//! history.push_add(drawing);
25//!
26//! // Undo removes it again, redo restores it
27//! assert_eq!(history.undo(&mut drawings), Some(1));
28//! assert!(drawings.is_empty());
29//! assert_eq!(history.redo(&mut drawings), Some(1));
30//! assert_eq!(drawings.len(), 1);
31//!
32//! // Before modifying a drawing, record its old state
33//! history.push_modify(1, drawings[0].clone());
34//!
35//! // After deleting a drawing, record the list index it was taken from
36//! let removed = drawings.remove(0);
37//! history.push_delete(removed, 0);
38//! assert!(history.can_undo());
39//! ```
40
41use crate::drawings::{ChartPoint, Drawing};
42use std::collections::HashSet;
43
44/// Command representing a reversible drawing operation
45#[derive(Debug, Clone)]
46pub(crate) enum DrawingCommand {
47    /// Drawings owned by one bulk edit, including their list order.
48    Snapshot {
49        /// Drawings at this side of the bulk edit, in list order, each with its list index.
50        drawings: Vec<(usize, Drawing)>,
51        /// Drawings owned by this edit; later host loads remain independent of it.
52        ids: Vec<usize>,
53    },
54    /// A drawing was added
55    Add(Drawing),
56    /// A drawing was deleted
57    Delete {
58        /// The deleted drawing
59        drawing: Drawing,
60        /// List index the drawing was taken from, where undo puts it back
61        index: usize,
62    },
63    /// Several drawing layers changed as one operation.
64    Reorder {
65        /// Drawing the user moved.
66        id: usize,
67        /// Previous layers of every affected drawing.
68        layers: Vec<(usize, i32)>,
69    },
70    /// A drawing was modified
71    Modify {
72        /// Drawing ID
73        id: usize,
74        /// State before modification
75        old_state: Drawing,
76    },
77}
78
79impl DrawingCommand {
80    /// Get the drawing ID affected by this command
81    pub(crate) fn drawing_id(&self) -> usize {
82        match self {
83            DrawingCommand::Snapshot { ids, .. } => ids.first().copied().unwrap_or(0),
84            DrawingCommand::Add(d) | DrawingCommand::Delete { drawing: d, .. } => d.id,
85            DrawingCommand::Modify { id, .. } | DrawingCommand::Reorder { id, .. } => *id,
86        }
87    }
88
89    /// Shift the bar indices at or after `from` for bars inserted in front of them
90    pub(crate) fn shift_bar_indices(&mut self, from: f32, delta: f32) {
91        match self {
92            DrawingCommand::Snapshot { drawings, .. } => {
93                for (_, drawing) in drawings {
94                    drawing.shift_bar_indices(from, delta);
95                }
96            }
97            DrawingCommand::Reorder { .. } => {}
98            DrawingCommand::Add(d) | DrawingCommand::Delete { drawing: d, .. } => {
99                d.shift_bar_indices(from, delta);
100            }
101            DrawingCommand::Modify { old_state, .. } => {
102                old_state.shift_bar_indices(from, delta);
103            }
104        }
105    }
106}
107
108/// History service for undo/redo operations
109#[derive(Debug, Clone, Default)]
110pub struct HistoryService {
111    /// Commands that can be undone
112    undo_stack: Vec<DrawingCommand>,
113    /// Commands that can be redone
114    redo_stack: Vec<DrawingCommand>,
115    /// Maximum stack size (0 = unlimited)
116    max_stack_size: usize,
117    affected_ids: Vec<usize>,
118}
119
120impl HistoryService {
121    pub(crate) fn visit_chart_points(&mut self, visit: &mut impl FnMut(&mut ChartPoint)) {
122        for command in self.undo_stack.iter_mut().chain(&mut self.redo_stack) {
123            match command {
124                DrawingCommand::Snapshot { drawings, .. } => {
125                    for (_, drawing) in drawings {
126                        drawing.chart_points.iter_mut().for_each(&mut *visit);
127                    }
128                }
129                DrawingCommand::Add(drawing)
130                | DrawingCommand::Delete { drawing, .. }
131                | DrawingCommand::Modify {
132                    old_state: drawing, ..
133                } => {
134                    drawing.chart_points.iter_mut().for_each(&mut *visit);
135                }
136                DrawingCommand::Reorder { .. } => {}
137            }
138        }
139    }
140
141    /// Create a new history service with default settings
142    pub fn new() -> Self {
143        Self {
144            undo_stack: Vec::new(),
145            redo_stack: Vec::new(),
146            affected_ids: Vec::new(),
147            max_stack_size: 100, // Reasonable default
148        }
149    }
150
151    /// Create a history that keeps at most `max_size` undo steps, dropping the oldest first;
152    /// `0` keeps every step.
153    pub fn with_max_size(max_size: usize) -> Self {
154        Self {
155            undo_stack: Vec::with_capacity(max_size.min(100)),
156            redo_stack: Vec::with_capacity(max_size.min(100)),
157            affected_ids: Vec::new(),
158            max_stack_size: max_size,
159        }
160    }
161
162    /// Record the drawings a bulk edit changes or removes, each with its list index before the
163    /// edit, as one undo step. Undo restores only these drawings.
164    pub fn push_snapshot(&mut self, drawings: Vec<(usize, Drawing)>) {
165        if !drawings.is_empty() {
166            let ids = drawings.iter().map(|(_, drawing)| drawing.id).collect();
167            self.push_command(DrawingCommand::Snapshot { drawings, ids });
168        }
169    }
170
171    /// Ids touched by the most recent undo or redo, for host synchronization.
172    pub(crate) fn affected_ids(&self) -> &[usize] {
173        &self.affected_ids
174    }
175
176    fn track_affected(&mut self, command: &DrawingCommand) {
177        self.affected_ids = match command {
178            DrawingCommand::Snapshot { ids, .. } => ids.clone(),
179            DrawingCommand::Reorder { layers, .. } => layers.iter().map(|(id, _)| *id).collect(),
180            _ => vec![command.drawing_id()],
181        };
182        self.affected_ids.sort_unstable();
183        self.affected_ids.dedup();
184    }
185
186    /// Record that a drawing was added
187    pub fn push_add(&mut self, drawing: Drawing) {
188        self.push_command(DrawingCommand::Add(drawing));
189    }
190
191    /// Removes the newest undo step if it added drawing `id`, returning whether it did.
192    pub(crate) fn pop_add(&mut self, id: usize) -> bool {
193        let last = matches!(self.undo_stack.last(), Some(DrawingCommand::Add(d)) if d.id == id);
194        if last {
195            self.undo_stack.pop();
196        }
197        last
198    }
199
200    /// Folds a text placement's lone edit or deletion into its original add, if both are still
201    /// the most recent commands. Other intervening operations retain their own undo steps.
202    pub(crate) fn merge_text_placement(
203        &mut self,
204        id: usize,
205        edit: &mut Self,
206        drawing: Option<&Drawing>,
207    ) -> bool {
208        let added = matches!(self.undo_stack.as_slice(), [DrawingCommand::Add(d)] if d.id == id);
209        let edited = matches!(edit.undo_stack.as_slice(), [DrawingCommand::Modify { id: changed, .. }] if *changed == id)
210            || matches!(edit.undo_stack.as_slice(), [DrawingCommand::Delete { drawing, .. }] if drawing.id == id);
211        if !added || !edited {
212            return false;
213        }
214        self.clear();
215        if let Some(drawing) = drawing {
216            self.push_add(drawing.clone());
217        }
218        edit.clear();
219        true
220    }
221
222    /// Record that a drawing was deleted from list index `index`
223    pub fn push_delete(&mut self, drawing: Drawing, index: usize) {
224        self.push_command(DrawingCommand::Delete { drawing, index });
225    }
226
227    /// Record that a drawing was modified
228    pub fn push_modify(&mut self, id: usize, old_state: Drawing) {
229        self.push_command(DrawingCommand::Modify { id, old_state });
230    }
231
232    /// Record a layer move, including the neighbours whose layers changed.
233    pub(crate) fn push_reorder(&mut self, id: usize, layers: Vec<(usize, i32)>) {
234        self.push_command(DrawingCommand::Reorder { id, layers });
235    }
236
237    /// Push a command to the undo stack
238    pub(crate) fn push_command(&mut self, command: DrawingCommand) {
239        // Clear redo stack on new action
240        self.redo_stack.clear();
241
242        // Enforce max size
243        if self.max_stack_size > 0 && self.undo_stack.len() >= self.max_stack_size {
244            self.undo_stack.remove(0);
245        }
246
247        self.undo_stack.push(command);
248    }
249
250    /// Undo the last operation
251    ///
252    /// Returns the ID of the affected drawing, if any.
253    pub fn undo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
254        let command = self.undo_stack.pop()?;
255        let affected_id = command.drawing_id();
256        self.track_affected(&command);
257
258        match command {
259            DrawingCommand::Snapshot {
260                drawings: before,
261                ids,
262            } => {
263                let current = restore_snapshot(drawings, before, &ids);
264                self.redo_stack.push(DrawingCommand::Snapshot {
265                    drawings: current,
266                    ids,
267                });
268            }
269            DrawingCommand::Reorder { id, mut layers } => {
270                for (id, layer) in &mut layers {
271                    if let Some(drawing) = drawings.iter_mut().find(|d| d.id == *id) {
272                        std::mem::swap(&mut drawing.z_order, layer);
273                    }
274                }
275                self.redo_stack.push(DrawingCommand::Reorder { id, layers });
276            }
277            DrawingCommand::Add(drawing) => {
278                // Undo add = remove the drawing
279                drawings.retain(|d| d.id != drawing.id);
280                self.redo_stack.push(DrawingCommand::Add(drawing));
281            }
282            DrawingCommand::Delete { drawing, index } => {
283                // Undo delete = restore the drawing at its list index
284                drawings.insert(index.min(drawings.len()), drawing.clone());
285                self.redo_stack
286                    .push(DrawingCommand::Delete { drawing, index });
287            }
288            DrawingCommand::Modify { id, old_state } => {
289                // Undo modify = restore old state
290                if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
291                    let current_state = current.clone();
292                    *current = old_state;
293                    self.redo_stack.push(DrawingCommand::Modify {
294                        id,
295                        old_state: current_state,
296                    });
297                }
298            }
299        }
300
301        Some(affected_id)
302    }
303
304    /// Redo the last undone operation
305    ///
306    /// Returns the ID of the affected drawing, if any.
307    pub fn redo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
308        let command = self.redo_stack.pop()?;
309        let affected_id = command.drawing_id();
310        self.track_affected(&command);
311
312        match command {
313            DrawingCommand::Snapshot {
314                drawings: before,
315                ids,
316            } => {
317                let current = restore_snapshot(drawings, before, &ids);
318                self.undo_stack.push(DrawingCommand::Snapshot {
319                    drawings: current,
320                    ids,
321                });
322            }
323            DrawingCommand::Reorder { id, mut layers } => {
324                for (id, layer) in &mut layers {
325                    if let Some(drawing) = drawings.iter_mut().find(|d| d.id == *id) {
326                        std::mem::swap(&mut drawing.z_order, layer);
327                    }
328                }
329                self.undo_stack.push(DrawingCommand::Reorder { id, layers });
330            }
331            DrawingCommand::Add(drawing) => {
332                // Redo add = add the drawing back
333                drawings.push(drawing.clone());
334                self.undo_stack.push(DrawingCommand::Add(drawing));
335            }
336            DrawingCommand::Delete { drawing, mut index } => {
337                // Redo delete = remove the drawing, recording where it was
338                if let Some(at) = drawings.iter().position(|d| d.id == drawing.id) {
339                    drawings.remove(at);
340                    index = at;
341                }
342                self.undo_stack
343                    .push(DrawingCommand::Delete { drawing, index });
344            }
345            DrawingCommand::Modify { id, old_state } => {
346                // Redo modify = apply the modification again
347                if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
348                    let current_state = current.clone();
349                    *current = old_state;
350                    self.undo_stack.push(DrawingCommand::Modify {
351                        id,
352                        old_state: current_state,
353                    });
354                }
355            }
356        }
357
358        Some(affected_id)
359    }
360
361    /// Check if undo is available
362    #[inline]
363    pub fn can_undo(&self) -> bool {
364        !self.undo_stack.is_empty()
365    }
366
367    /// Check if redo is available
368    #[inline]
369    pub fn can_redo(&self) -> bool {
370        !self.redo_stack.is_empty()
371    }
372
373    /// Get the number of undo operations available
374    #[inline]
375    pub fn undo_count(&self) -> usize {
376        self.undo_stack.len()
377    }
378
379    /// Get the number of redo operations available
380    #[inline]
381    pub fn redo_count(&self) -> usize {
382        self.redo_stack.len()
383    }
384
385    /// Clear all history
386    pub fn clear(&mut self) {
387        self.undo_stack.clear();
388        self.redo_stack.clear();
389        self.affected_ids.clear();
390    }
391
392    /// Shift the bar indices at or after `from` in history (for bars inserted in front of them)
393    pub fn shift_bar_indices(&mut self, from: f32, delta: f32) {
394        for cmd in &mut self.undo_stack {
395            cmd.shift_bar_indices(from, delta);
396        }
397        for cmd in &mut self.redo_stack {
398            cmd.shift_bar_indices(from, delta);
399        }
400    }
401
402    /// Prune undo history entries for drawings that no longer exist.
403    ///
404    /// Only the undo stack is pruned against the live id set. The redo stack is
405    /// intentionally left intact: a redo entry legitimately references a drawing
406    /// that is absent from the live set. After an `Add` is undone, the drawing
407    /// is removed from the live set but its re-adding command sits on the redo
408    /// stack; pruning the redo stack against live ids would silently drop that
409    /// entry and lose the redo. The redo stack is the authoritative record of
410    /// what redo would restore, so it is the wrong place to enforce live-id
411    /// membership.
412    pub fn prune(&mut self, existing_ids: &HashSet<usize>) {
413        self.undo_stack.retain(|cmd| {
414            matches!(cmd, DrawingCommand::Snapshot { .. })
415                || existing_ids.contains(&cmd.drawing_id())
416        });
417    }
418
419    /// Get description of the last undoable action (for UI)
420    pub fn last_undo_description(&self) -> Option<&'static str> {
421        self.undo_stack.last().map(|cmd| match cmd {
422            DrawingCommand::Snapshot { .. } => "Edit Drawings",
423            DrawingCommand::Add(_) => "Add Drawing",
424            DrawingCommand::Delete { .. } => "Delete Drawing",
425            DrawingCommand::Modify { .. } => "Modify Drawing",
426            DrawingCommand::Reorder { .. } => "Reorder Drawings",
427        })
428    }
429
430    /// Get description of the last redoable action (for UI)
431    pub fn last_redo_description(&self) -> Option<&'static str> {
432        self.redo_stack.last().map(|cmd| match cmd {
433            DrawingCommand::Snapshot { .. } => "Edit Drawings",
434            DrawingCommand::Add(_) => "Add Drawing",
435            DrawingCommand::Delete { .. } => "Delete Drawing",
436            DrawingCommand::Modify { .. } => "Modify Drawing",
437            DrawingCommand::Reorder { .. } => "Reorder Drawings",
438        })
439    }
440}
441
442// Keep independent drawings and their list positions outside the bulk command's history.
443// Owned drawings still listed are restored in place; the others go back to their recorded
444// list index, clamped, and the drawings taken out keep theirs for the inverse command.
445fn restore_snapshot(
446    live: &mut Vec<Drawing>,
447    mut before: Vec<(usize, Drawing)>,
448    ids: &[usize],
449) -> Vec<(usize, Drawing)> {
450    let mut current = Vec::new();
451    let mut restored = Vec::new();
452    for (index, drawing) in live.drain(..).enumerate() {
453        if !ids.contains(&drawing.id) {
454            restored.push(drawing);
455            continue;
456        }
457        if let Some(at) = before.iter().position(|(_, old)| old.id == drawing.id) {
458            restored.push(before.remove(at).1);
459        }
460        current.push((index, drawing));
461    }
462    for (index, drawing) in before {
463        restored.insert(index.min(restored.len()), drawing);
464    }
465    *live = restored;
466    current
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use crate::drawings::{ChartPoint, DrawingToolType};
473
474    fn make_drawing(id: usize) -> Drawing {
475        Drawing::new(id, DrawingToolType::TrendLine)
476    }
477
478    fn drawing(id: usize, stroke_width: f32) -> Drawing {
479        let mut d = make_drawing(id);
480        d.stroke_width = stroke_width;
481        d
482    }
483
484    /// The live list as (id, stroke width, z-order), in list order, so a test sees which
485    /// drawings exist, which state each one is in and which is on top among equal z-orders.
486    fn state(drawings: &[Drawing]) -> Vec<(usize, f32, i32)> {
487        drawings
488            .iter()
489            .map(|d| (d.id, d.stroke_width, d.z_order))
490            .collect()
491    }
492
493    /// Undo of each command returns the live list to its state and order before
494    /// the action, redo returns it to the state after, and the redone command can
495    /// be undone again.
496    #[test]
497    fn undo_and_redo_restore_each_command() {
498        type Record = fn(&mut HistoryService);
499        let cases: [(&str, Vec<Drawing>, Vec<Drawing>, Record); 7] = [
500            (
501                "partial bulk edit",
502                vec![drawing(2, 9.0), drawing(1, 2.0)],
503                vec![drawing(2, 9.0), drawing(1, 8.0)],
504                |h| h.push_snapshot(vec![(1, drawing(1, 2.0))]),
505            ),
506            (
507                "bulk edit and order",
508                vec![drawing(1, 2.0), drawing(2, 3.0)],
509                vec![drawing(2, 8.0)],
510                |h| h.push_snapshot(vec![(0, drawing(1, 2.0)), (1, drawing(2, 3.0))]),
511            ),
512            (
513                "paste",
514                vec![drawing(2, 1.0), drawing(3, 1.0)],
515                vec![drawing(2, 1.0), drawing(3, 1.0), drawing(1, 1.0)],
516                |h| {
517                    h.push_command(DrawingCommand::Snapshot {
518                        drawings: Vec::new(),
519                        ids: vec![1],
520                    })
521                },
522            ),
523            (
524                "reorder",
525                vec![drawing(1, 1.0), drawing(2, 1.0)],
526                vec![
527                    Drawing {
528                        z_order: 1,
529                        ..drawing(1, 1.0)
530                    },
531                    Drawing {
532                        z_order: 2,
533                        ..drawing(2, 1.0)
534                    },
535                ],
536                |h| h.push_reorder(1, vec![(1, 0), (2, 0)]),
537            ),
538            (
539                "add",
540                vec![drawing(2, 1.0)],
541                vec![drawing(2, 1.0), drawing(1, 1.0)],
542                |h| h.push_add(drawing(1, 1.0)),
543            ),
544            (
545                "delete",
546                vec![drawing(2, 1.0), drawing(1, 1.0), drawing(3, 1.0)],
547                vec![drawing(2, 1.0), drawing(3, 1.0)],
548                |h| h.push_delete(drawing(1, 1.0), 1),
549            ),
550            (
551                "modify",
552                vec![drawing(1, 2.0)],
553                vec![drawing(1, 5.0)],
554                |h| h.push_modify(1, drawing(1, 2.0)),
555            ),
556        ];
557
558        for (name, mut before, mut after, record) in cases {
559            let mut history = HistoryService::new();
560            let mut drawings = after.clone();
561            record(&mut history);
562            if name == "bulk edit and order" {
563                // A later host load must survive undo and redo of an earlier user command.
564                let independent = drawing(3, 7.0);
565                before.push(independent.clone());
566                after.push(independent.clone());
567                drawings.push(independent);
568            }
569
570            assert_eq!(history.undo(&mut drawings), Some(1), "{name}: undo id");
571            assert_eq!(state(&drawings), state(&before), "{name}: after undo");
572            assert_eq!(history.redo(&mut drawings), Some(1), "{name}: redo id");
573            assert_eq!(state(&drawings), state(&after), "{name}: after redo");
574            assert_eq!(history.undo(&mut drawings), Some(1), "{name}: undo again");
575            assert_eq!(state(&drawings), state(&before), "{name}: after undo again");
576        }
577    }
578
579    #[test]
580    fn test_new_action_clears_redo() {
581        let mut history = HistoryService::new();
582        let mut drawings = Vec::new();
583
584        // Add, undo, add new - should clear redo
585        let d1 = make_drawing(1);
586        drawings.push(d1.clone());
587        history.push_add(d1);
588
589        history.undo(&mut drawings);
590        assert!(history.can_redo());
591
592        let d2 = make_drawing(2);
593        drawings.push(d2.clone());
594        history.push_add(d2);
595
596        assert!(!history.can_redo()); // Redo cleared
597    }
598
599    /// The cap drops the oldest command, so the newest ones stay undoable.
600    #[test]
601    fn test_max_stack_size() {
602        let mut history = HistoryService::with_max_size(3);
603        let mut drawings = Vec::new();
604
605        for i in 1..=5 {
606            let d = make_drawing(i);
607            drawings.push(d.clone());
608            history.push_add(d);
609        }
610
611        for expected in [5, 4, 3] {
612            assert_eq!(history.undo(&mut drawings), Some(expected));
613        }
614        assert_eq!(history.undo(&mut drawings), None);
615    }
616
617    /// When history is prepended, commands on both stacks move with the bars,
618    /// so a later undo or redo puts the drawing back at its shifted position.
619    #[test]
620    fn test_shift_bar_indices() {
621        let anchored = |id, bar_idx| {
622            let mut d = make_drawing(id);
623            d.chart_points.push(ChartPoint::new(bar_idx, 50.0));
624            d
625        };
626        let mut history = HistoryService::new();
627        let mut drawings = vec![anchored(1, 120.0), anchored(2, 200.0)];
628
629        history.push_modify(1, anchored(1, 100.0));
630        history.push_add(anchored(2, 200.0));
631        history.undo(&mut drawings); // Add(2) moves to the redo stack
632
633        history.shift_bar_indices(f32::NEG_INFINITY, 50.0);
634
635        // Modify(1) sat on the undo stack: undo restores its old state, shifted.
636        assert_eq!(history.undo(&mut drawings), Some(1));
637        assert_eq!(drawings[0].chart_points[0].bar_idx, 150.0);
638        history.redo(&mut drawings); // reapplies Modify(1)
639        // Add(2) was on the redo stack: redo re-adds it at the shifted bar.
640        assert_eq!(history.redo(&mut drawings), Some(2));
641        assert_eq!(drawings[1].chart_points[0].bar_idx, 250.0);
642    }
643
644    /// Undoing an `Add` parks the re-add command on the redo stack while the
645    /// drawing is absent from the live set. A subsequent `prune` against the
646    /// live ids must not drop that redo entry, or the redo would be silently
647    /// lost. This guards the invariant that redo survives pruning.
648    #[test]
649    fn test_undo_add_then_prune_preserves_redo() {
650        let mut history = HistoryService::new();
651        let mut drawings = Vec::new();
652
653        let drawing = make_drawing(1);
654        drawings.push(drawing.clone());
655        history.push_add(drawing);
656
657        // Undo the add: drawing leaves the live set, redo becomes available.
658        history.undo(&mut drawings);
659        assert!(drawings.is_empty());
660        assert!(history.can_redo());
661
662        // Prune against the (now empty) live id set, as a delete-driven cleanup
663        // would. The undone-add redo entry must survive.
664        history.prune(&HashSet::new());
665        assert!(
666            history.can_redo(),
667            "redo entry for an undone Add must survive prune"
668        );
669
670        // Redo must re-add the drawing.
671        let affected = history.redo(&mut drawings);
672        assert_eq!(affected, Some(1));
673        assert_eq!(drawings.len(), 1);
674        assert_eq!(drawings[0].id, 1);
675    }
676}