From 1bfd3197581ee72adc693d24b8d00bd24e950f1e Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 16 Oct 2026 09:02:31 +0200 Subject: [PATCH] use history variant --- gpx-rs/src/api/commands.rs | 181 ++++++++++++-------- gpx-rs/src/api/dto.rs | 4 +- gpx-rs/src/api/queries.rs | 70 ++++---- gpx-rs/src/engine/state/history/global.rs | 2 + gpx-rs/src/engine/state/history/mod.rs | 5 + gpx-rs/src/engine/state/history/per_file.rs | 2 + tauri/src/commands/edit.rs | 6 +- tauri/src/lib.rs | 4 + tauri/src/state.rs | 3 +- tauri/src/store.rs | 5 +- tauri/src/tests.rs | 11 +- tauri/src/workspace.rs | 3 +- wasm/src/command/routing.rs | 2 +- wasm/src/command/tools.rs | 4 +- 14 files changed, 181 insertions(+), 121 deletions(-) diff --git a/gpx-rs/src/api/commands.rs b/gpx-rs/src/api/commands.rs index 71bfd7272..53a7e1e80 100644 --- a/gpx-rs/src/api/commands.rs +++ b/gpx-rs/src/api/commands.rs @@ -3,17 +3,17 @@ use uuid::Uuid; use super::dto::*; -use crate::{self as engine, Action, Command, Engine, LngLat, LngLatBounds}; +use crate::{self as engine, Action, Command, Engine, History, LngLat, LngLatBounds}; /// Executes an action and tells what it did. -pub fn execute(engine: &mut Engine, action: Action) -> Outcome { +pub fn execute(engine: &mut Engine, action: Action) -> Outcome { match engine.execute(action) { Ok(outcome) => Outcome::new(&outcome, None), Err(error) => Outcome::failed(error.to_string()), } } -pub fn edit(engine: &mut Engine, command: Command) -> Outcome { +pub fn edit(engine: &mut Engine, command: Command) -> Outcome { execute(engine, Action::Edit(command)) } @@ -24,7 +24,11 @@ fn file_ids(ids: &[Uuid]) -> Vec { // Selection /// Selects files. Unknown files are ignored, and selecting nothing deselects everything. -pub fn select(engine: &mut Engine, file_ids_: &[Uuid], mode: engine::SelectMode) -> Outcome { +pub fn select( + engine: &mut Engine, + file_ids_: &[Uuid], + mode: engine::SelectMode, +) -> Outcome { select_elements( engine, engine::Selection::File { @@ -36,8 +40,8 @@ pub fn select(engine: &mut Engine, file_ids_: &[Uuid], mode: engine::SelectMode) /// Selects elements: they are merged with the selection when it holds elements of the same kind in /// the same place (same file, same track for segments), otherwise they replace it. -pub fn select_elements( - engine: &mut Engine, +pub fn select_elements( + engine: &mut Engine, selection: engine::Selection, mode: engine::SelectMode, ) -> Outcome { @@ -47,19 +51,19 @@ pub fn select_elements( /// Selects all the elements of the same kind as the selected ones, in the same place: all the /// files, the tracks of the file, the segments of the track, the waypoints of the file. All the /// files when nothing is selected. -pub fn select_all(engine: &mut Engine) -> Outcome { +pub fn select_all(engine: &mut Engine) -> Outcome { execute(engine, Action::SelectAll) } /// Moves the selection to the next (`down`) or previous element of the same kind, like the arrow /// keys do in the file list. With `add` (shift + arrow), the element is added to the selection, /// otherwise it replaces it. -pub fn arrow_select(engine: &mut Engine, down: bool, add: bool) -> Outcome { +pub fn arrow_select(engine: &mut Engine, down: bool, add: bool) -> Outcome { execute(engine, Action::ArrowSelect { down, add }) } -pub fn select_tracks( - engine: &mut Engine, +pub fn select_tracks( + engine: &mut Engine, file_id: Uuid, track_ids: &[Uuid], mode: engine::SelectMode, @@ -71,8 +75,8 @@ pub fn select_tracks( select_elements(engine, selection, mode) } -pub fn select_segments( - engine: &mut Engine, +pub fn select_segments( + engine: &mut Engine, file_id: Uuid, track_id: Uuid, segment_ids: &[Uuid], @@ -91,8 +95,8 @@ pub fn select_segments( } /// Selects the node standing for all the waypoints of a file. -pub fn select_waypoint_group( - engine: &mut Engine, +pub fn select_waypoint_group( + engine: &mut Engine, file_id: Uuid, mode: engine::SelectMode, ) -> Outcome { @@ -102,8 +106,8 @@ pub fn select_waypoint_group( select_elements(engine, selection, mode) } -pub fn select_waypoints( - engine: &mut Engine, +pub fn select_waypoints( + engine: &mut Engine, file_id: Uuid, waypoint_ids: &[Uuid], mode: engine::SelectMode, @@ -121,11 +125,11 @@ pub fn select_waypoints( // History -pub fn undo(engine: &mut Engine) -> Outcome { +pub fn undo(engine: &mut Engine) -> Outcome { execute(engine, Action::Undo) } -pub fn redo(engine: &mut Engine) -> Outcome { +pub fn redo(engine: &mut Engine) -> Outcome { execute(engine, Action::Redo) } @@ -133,8 +137,8 @@ pub fn redo(engine: &mut Engine) -> Outcome { /// Creates a file and selects it. With `lng` and `lat`, it starts with a track and a segment that /// hold that trackpoint (at `ele`, 0 by default). -pub fn new_file( - engine: &mut Engine, +pub fn new_file( + engine: &mut Engine, name: &str, lng: Option, lat: Option, @@ -152,8 +156,8 @@ pub fn new_file( /// `lengths` being the number of bytes of each one. `names`: the name of each file when its data /// has none (the name on disk, without extension). The files that cannot be read are skipped, the /// first one that was read is selected. -pub fn load_files( - engine: &mut Engine, +pub fn load_files( + engine: &mut Engine, data: &[u8], lengths: &[usize], names: &[String], @@ -181,7 +185,11 @@ pub fn load_files( /// Brings changes of the files from the outside in (see [`engine::SyncFiles`]): each file is /// `(id, data, name)`, and replaces the file of that id, or is added if there is none. The files to /// `remove` are deleted. The selection is left alone. -pub fn sync_files(engine: &mut Engine, files: &[(Uuid, &[u8], &str)], remove: &[Uuid]) -> Outcome { +pub fn sync_files( + engine: &mut Engine, + files: &[(Uuid, &[u8], &str)], + remove: &[Uuid], +) -> Outcome { let files = files .iter() .map(|(id, data, name)| engine::ExternalFile { @@ -199,22 +207,22 @@ pub fn sync_files(engine: &mut Engine, files: &[(Uuid, &[u8], &str)], remove: &[ ) } -pub fn duplicate(engine: &mut Engine) -> Outcome { +pub fn duplicate(engine: &mut Engine) -> Outcome { edit(engine, Command::Duplicate(engine::Duplicate)) } /// Deletes the selected elements. With `whole_files`, the files holding the selected elements are /// deleted instead, even if only a track or a waypoint is selected. -pub fn delete(engine: &mut Engine, whole_files: bool) -> Outcome { +pub fn delete(engine: &mut Engine, whole_files: bool) -> Outcome { edit(engine, Command::Delete(engine::Delete { whole_files })) } -pub fn delete_all(engine: &mut Engine) -> Outcome { +pub fn delete_all(engine: &mut Engine) -> Outcome { edit(engine, Command::DeleteAll(engine::DeleteAll)) } /// Moves the files to `index` among the other files, in the given order (not undoable). -pub fn reorder(engine: &mut Engine, file_ids_: &[Uuid], index: usize) -> Outcome { +pub fn reorder(engine: &mut Engine, file_ids_: &[Uuid], index: usize) -> Outcome { execute( engine, Action::Reorder { @@ -225,12 +233,12 @@ pub fn reorder(engine: &mut Engine, file_ids_: &[Uuid], index: usize) -> Outcome } /// Puts the selected elements in the clipboard. Nothing happens when nothing is selected. -pub fn copy(engine: &mut Engine) -> Outcome { +pub fn copy(engine: &mut Engine) -> Outcome { execute(engine, Action::Copy) } /// Like `copy`, but the elements are moved when they are pasted. -pub fn cut(engine: &mut Engine) -> Outcome { +pub fn cut(engine: &mut Engine) -> Outcome { execute(engine, Action::Cut) } @@ -238,7 +246,7 @@ pub fn cut(engine: &mut Engine) -> Outcome { /// elements as they were when they were copied or cut, even if they were changed or deleted since. /// It can be pasted several times: what was copied is copied again, what was cut is moved the first /// time (and the clipboard is not cut after that), then it is copied. -pub fn paste(engine: &mut Engine) -> Outcome { +pub fn paste(engine: &mut Engine) -> Outcome { edit(engine, Command::Paste(engine::Paste)) } @@ -249,7 +257,11 @@ pub fn paste(engine: &mut Engine) -> Outcome { /// of a file; segments among the files (each becomes a file), the tracks of a file (each becomes /// a track) or the segments of a track; waypoints, or all the waypoints of a file, among the /// waypoints of a file. -pub fn move_elements(engine: &mut Engine, what: Selection, to: MoveTarget) -> Outcome { +pub fn move_elements( + engine: &mut Engine, + what: Selection, + to: MoveTarget, +) -> Outcome { edit( engine, Command::Move(engine::Move { @@ -259,13 +271,13 @@ pub fn move_elements(engine: &mut Engine, what: Selection, to: MoveTarget) -> Ou ) } -pub fn metadata(engine: &mut Engine, name: &str, desc: &str) -> Outcome { +pub fn metadata(engine: &mut Engine, name: &str, desc: &str) -> Outcome { edit(engine, Command::Metadata(engine::Metadata { name, desc })) } /// Only the given fields are changed. -pub fn style( - engine: &mut Engine, +pub fn style( + engine: &mut Engine, color: Option<&str>, opacity: Option, width: Option, @@ -280,11 +292,11 @@ pub fn style( ) } -pub fn new_track(engine: &mut Engine) -> Outcome { +pub fn new_track(engine: &mut Engine) -> Outcome { edit(engine, Command::NewTrack(engine::NewTrack)) } -pub fn new_track_segment(engine: &mut Engine) -> Outcome { +pub fn new_track_segment(engine: &mut Engine) -> Outcome { edit(engine, Command::NewTrackSegment(engine::NewTrackSegment)) } @@ -292,8 +304,8 @@ pub fn new_track_segment(engine: &mut Engine) -> Outcome { /// Adds a waypoint to each selected file (or to the file of the selected elements). #[allow(clippy::too_many_arguments)] -pub fn new_waypoint( - engine: &mut Engine, +pub fn new_waypoint( + engine: &mut Engine, lng: f64, lat: f64, ele: f64, @@ -317,8 +329,8 @@ pub fn new_waypoint( } /// Moves a waypoint of a file, whatever is selected. -pub fn move_waypoint( - engine: &mut Engine, +pub fn move_waypoint( + engine: &mut Engine, file_id: Uuid, waypoint_id: Uuid, lng: f64, @@ -338,7 +350,11 @@ pub fn move_waypoint( } /// Deletes a waypoint of a file, whatever is selected. -pub fn delete_waypoint(engine: &mut Engine, file_id: Uuid, waypoint_id: Uuid) -> Outcome { +pub fn delete_waypoint( + engine: &mut Engine, + file_id: Uuid, + waypoint_id: Uuid, +) -> Outcome { edit( engine, Command::DeleteWaypoint(engine::DeleteWaypoint { @@ -351,8 +367,8 @@ pub fn delete_waypoint(engine: &mut Engine, file_id: Uuid, waypoint_id: Uuid) -> /// Changes a waypoint of a file, whatever is selected: its name, description, icon, link, /// position and elevation. #[allow(clippy::too_many_arguments)] -pub fn update_waypoint( - engine: &mut Engine, +pub fn update_waypoint( + engine: &mut Engine, file_id: Uuid, waypoint_id: Uuid, lng: f64, @@ -381,19 +397,19 @@ pub fn update_waypoint( // Tools -pub fn reverse(engine: &mut Engine) -> Outcome { +pub fn reverse(engine: &mut Engine) -> Outcome { edit(engine, Command::Reverse(engine::Reverse)) } /// Adds to each selected segment a reversed copy of itself (without repeating its last point), so /// that it goes back to where it started. -pub fn round_trip(engine: &mut Engine) -> Outcome { +pub fn round_trip(engine: &mut Engine) -> Outcome { edit(engine, Command::RoundTrip(engine::RoundTrip)) } /// Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its /// segments one after the other, and removes the others (and what is left empty). -pub fn crop(engine: &mut Engine, start: u32, end: u32) -> Outcome { +pub fn crop(engine: &mut Engine, start: u32, end: u32) -> Outcome { if start > end { return Outcome::failed(format!("the range {start}..{end} is reversed")); } @@ -402,14 +418,19 @@ pub fn crop(engine: &mut Engine, start: u32, end: u32) -> Outcome { /// Removes the trackpoints of the selection that are less than `tolerance` meters away from the /// line of their neighbours, except the ends of the segments. -pub fn reduce(engine: &mut Engine, tolerance: f64) -> Outcome { +pub fn reduce(engine: &mut Engine, tolerance: f64) -> Outcome { edit(engine, Command::Reduce(engine::Reduce { tolerance })) } /// Sets the timestamps of the selection, which starts at `start_time` (ms since the epoch): the /// trackpoints that have some keep their durations multiplied by `ratio`, the others follow the /// previous one at `speed` (km/h). -pub fn change_timestamps(engine: &mut Engine, start_time: f64, speed: f64, ratio: f64) -> Outcome { +pub fn change_timestamps( + engine: &mut Engine, + start_time: f64, + speed: f64, + ratio: f64, +) -> Outcome { edit( engine, Command::Time(engine::Time { @@ -421,8 +442,8 @@ pub fn change_timestamps(engine: &mut Engine, start_time: f64, speed: f64, ratio /// Makes up timestamps for the selection, which starts at `start_time` (ms since the epoch) and /// lasts `total_time` seconds. -pub fn create_artificial_timestamps( - engine: &mut Engine, +pub fn create_artificial_timestamps( + engine: &mut Engine, start_time: f64, total_time: f64, ) -> Outcome { @@ -436,38 +457,43 @@ pub fn create_artificial_timestamps( } /// Merges the selection into its first element. `remove_gaps` brings the parts that are connected -/// closer in time, if they have timestamps. With `keep_originals`, merged files are left as they -/// are and the result is a new file. -pub fn merge( - engine: &mut Engine, +/// closer in time, if they have timestamps. Merged files are kept (and the result is a new file) if +/// the history needs it, see [`History::KEEPS_ORIGINALS`]. +pub fn merge( + engine: &mut Engine, type_: engine::MergeType, remove_gaps: bool, - keep_originals: bool, ) -> Outcome { edit( engine, Command::Merge(engine::Merge { type_, remove_gaps, - keep_originals, + keep_originals: H::KEEPS_ORIGINALS, }), ) } -/// With `keep_originals`, extracted files are left as they are and the result is new files. -pub fn extract(engine: &mut Engine, keep_originals: bool) -> Outcome { - edit(engine, Command::Extract(engine::Extract { keep_originals })) +/// Extracted files are kept (and the result is new files) if the history needs it, see +/// [`History::KEEPS_ORIGINALS`]. +pub fn extract(engine: &mut Engine) -> Outcome { + edit( + engine, + Command::Extract(engine::Extract { + keep_originals: H::KEEPS_ORIGINALS, + }), + ) } /// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint. -pub fn elevation(engine: &mut Engine, ele: &[f64]) -> Outcome { +pub fn elevation(engine: &mut Engine, ele: &[f64]) -> Outcome { edit(engine, Command::Elevation(engine::Elevation { ele })) } /// Removes the trackpoints and/or waypoints of the selection inside (or outside) a rectangle. #[allow(clippy::too_many_arguments)] -pub fn clean( - engine: &mut Engine, +pub fn clean( + engine: &mut Engine, west: f64, south: f64, east: f64, @@ -503,10 +529,10 @@ pub fn clean( // that use them do nothing if the selection is not the one of that revision anymore. /// Makes the call `f`, unless the selection is not the one of the revision anymore. -fn at_revision( - engine: &mut Engine, +fn at_revision( + engine: &mut Engine, revision: u32, - f: impl FnOnce(&mut Engine) -> Outcome, + f: impl FnOnce(&mut Engine) -> Outcome, ) -> Outcome { if engine.routing().revision == revision { f(engine) @@ -520,8 +546,8 @@ fn at_revision( /// surface, highway, SAC scale and MTB scale of the new points, as intervals, and `anchors` the /// indices among the new points of the ones that become anchors. #[allow(clippy::too_many_arguments)] -pub fn route( - engine: &mut Engine, +pub fn route( + engine: &mut Engine, revision: u32, start: u32, end: u32, @@ -555,7 +581,12 @@ pub fn route( /// Makes an anchor of the point of the selected segments that is the closest to the coordinates, /// inserting a trackpoint on the path if none is there. -pub fn insert_anchor(engine: &mut Engine, revision: u32, lng: f64, lat: f64) -> Outcome { +pub fn insert_anchor( + engine: &mut Engine, + revision: u32, + lng: f64, + lat: f64, +) -> Outcome { at_revision(engine, revision, |engine| { edit( engine, @@ -566,7 +597,7 @@ pub fn insert_anchor(engine: &mut Engine, revision: u32, lng: f64, lat: f64) -> /// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the /// trackpoints before it go to the end, and the loop closes on it). -pub fn change_loop_start(engine: &mut Engine, revision: u32, index: u32) -> Outcome { +pub fn change_loop_start(engine: &mut Engine, revision: u32, index: u32) -> Outcome { at_revision(engine, revision, |engine| { edit( engine, @@ -576,14 +607,14 @@ pub fn change_loop_start(engine: &mut Engine, revision: u32, index: u32) -> Outc } /// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the -/// selection in two, there: the trackpoint ends the first part and starts the second one. With -/// `keep_originals`, a file that is cut is left as it is and its two parts are new files. -pub fn split( - engine: &mut Engine, +/// selection in two, there: the trackpoint ends the first part and starts the second one. A file that +/// is cut is kept (and its two parts are new files) if the history needs it, see +/// [`History::KEEPS_ORIGINALS`]. +pub fn split( + engine: &mut Engine, revision: u32, at: u32, split_type: engine::SplitType, - keep_originals: bool, ) -> Outcome { at_revision(engine, revision, |engine| { edit( @@ -591,7 +622,7 @@ pub fn split( Command::Split(engine::Split { at, split_type, - keep_originals, + keep_originals: H::KEEPS_ORIGINALS, }), ) }) diff --git a/gpx-rs/src/api/dto.rs b/gpx-rs/src/api/dto.rs index a3696df28..6de45e207 100644 --- a/gpx-rs/src/api/dto.rs +++ b/gpx-rs/src/api/dto.rs @@ -3,7 +3,7 @@ //! are numbers (ms since the epoch), absent fields are skipped. use crate as engine; -use engine::FileId; +use engine::{FileId, History}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -49,7 +49,7 @@ impl Outcome { /// The outcome of a frontend that knows nothing of the engine: all the files are added, and the /// order, the selection and the clipboard are to be read. - pub fn everything(engine: &engine::Engine) -> Self { + pub fn everything(engine: &engine::Engine) -> Self { Self { changed: true, order_changed: true, diff --git a/gpx-rs/src/api/queries.rs b/gpx-rs/src/api/queries.rs index fd05ed0f0..a65c65358 100644 --- a/gpx-rs/src/api/queries.rs +++ b/gpx-rs/src/api/queries.rs @@ -8,60 +8,64 @@ use uuid::Uuid; use super::dto::*; -use crate::{self as engine, Engine, FileId}; +use crate::{self as engine, Engine, FileId, History}; /// Ids of the files in display order. -pub fn file_order(engine: &Engine) -> Vec { +pub fn file_order(engine: &Engine) -> Vec { ids(engine.order()) } /// Structure of a file, `None` if the id is unknown. -pub fn file_structure(engine: &Engine, file_id: Uuid) -> Option { +pub fn file_structure(engine: &Engine, file_id: Uuid) -> Option { engine .file_structure(&FileId(file_id)) .map(FileStructure::new) } /// Global statistics of a file, `None` if the id is unknown. -pub fn file_statistics(engine: &Engine, file_id: Uuid) -> Option { +pub fn file_statistics(engine: &Engine, file_id: Uuid) -> Option { engine .file_statistics(&FileId(file_id)) .map(|stats| GlobalStatistics::new(&stats)) } /// The current selection. -pub fn selection(engine: &Engine) -> Selection { +pub fn selection(engine: &Engine) -> Selection { engine.selection().into() } /// What was copied or cut and is waiting to be pasted. -pub fn clipboard(engine: &Engine) -> Option { +pub fn clipboard(engine: &Engine) -> Option { engine.clipboard().map(Clipboard::new) } -pub fn can_undo(engine: &Engine) -> bool { +pub fn can_undo(engine: &Engine) -> bool { engine.can_undo() } -pub fn can_redo(engine: &Engine) -> bool { +pub fn can_redo(engine: &Engine) -> bool { engine.can_redo() } /// Whether the clipboard can be pasted with the current selection. -pub fn can_paste(engine: &Engine) -> bool { +pub fn can_paste(engine: &Engine) -> bool { engine.can_paste() } /// All the data of a waypoint of a file, `None` if it does not exist. -pub fn waypoint(engine: &Engine, file_id: Uuid, waypoint_id: Uuid) -> Option { +pub fn waypoint( + engine: &Engine, + file_id: Uuid, + waypoint_id: Uuid, +) -> Option { engine .waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id)) .map(WaypointDetails::new) } /// A trackpoint of a segment of a file, `None` if it does not exist. -pub fn trackpoint( - engine: &Engine, +pub fn trackpoint( + engine: &Engine, file_id: Uuid, segment_id: Uuid, index: usize, @@ -75,8 +79,8 @@ pub fn trackpoint( /// trackpoints to keep, and with `as_route` that the segments are written as routes (`rte`) /// instead of tracks: the anchors are the route points, and the trackpoints between them the /// points of their path. -pub fn export_file( - engine: &Engine, +pub fn export_file( + engine: &Engine, file_id: Uuid, options: engine::ExportOptions, ) -> Option> { @@ -86,7 +90,7 @@ pub fn export_file( /// The data that some files have, so that the ones that none has are not offered when exporting /// them: a bit for the time (1), heart rate (2), cadence (4), temperature (8), power (16) and the /// OpenStreetMap data (32). -pub fn exportable_data(engine: &Engine, file_ids: &[Uuid]) -> u8 { +pub fn exportable_data(engine: &Engine, file_ids: &[Uuid]) -> u8 { let ids: Vec = file_ids.iter().copied().map(FileId).collect(); let data = engine.exportable_data(&ids); [ @@ -99,7 +103,7 @@ pub fn exportable_data(engine: &Engine, file_ids: &[Uuid]) -> u8 { /// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance /// (m) of `reduce` that keeps it: infinite for the ends of the segments. -pub fn reduction_distances(engine: &Engine) -> Vec { +pub fn reduction_distances(engine: &Engine) -> Vec { engine.reduction_distances() } @@ -107,25 +111,29 @@ pub fn reduction_distances(engine: &Engine) -> Vec { // structures with the previous ones to know which ones actually changed. /// Coordinates of the trackpoints of a segment (empty if the id is unknown). -pub fn segment_coordinates(engine: &Engine, segment_id: Uuid) -> &[f64] { +pub fn segment_coordinates(engine: &Engine, segment_id: Uuid) -> &[f64] { engine.segment_coordinates(&engine::TrackSegmentId(segment_id)) } /// Coordinates of the waypoints of a file (empty if the id is unknown). -pub fn waypoint_coordinates(engine: &Engine, file_id: Uuid) -> &[f64] { +pub fn waypoint_coordinates(engine: &Engine, file_id: Uuid) -> &[f64] { engine.waypoint_coordinates(&FileId(file_id)) } // Statistics of the selection /// Global statistics of the whole selection (the buffers below cover it). -pub fn selection_statistics(engine: &Engine) -> GlobalStatistics { +pub fn selection_statistics(engine: &Engine) -> GlobalStatistics { GlobalStatistics::new(&engine.statistics().global) } /// Global statistics of the trackpoints from `start` to `end` (both included) of the selection, /// as indexed by the buffers. `None` when the range is not in the selection. -pub fn slice_statistics(engine: &Engine, start: usize, end: usize) -> Option { +pub fn slice_statistics( + engine: &Engine, + start: usize, + end: usize, +) -> Option { engine .statistics() .slice(start, end) @@ -134,7 +142,7 @@ pub fn slice_statistics(engine: &Engine, start: usize, end: usize) -> Option {$( - pub fn $name(engine: &Engine) -> &[f64] { + pub fn $name(engine: &Engine) -> &[f64] { &engine.statistics().$name } )*}; @@ -158,7 +166,7 @@ macro_rules! optional_buffers { /// `None` when no trackpoint of the selection has the value, else an entry per trackpoint, /// the missing ones being NaN (`NO_TIME` for the timestamps, 0 before the first trackpoint /// that has them for the cumulative distances and durations). - pub fn $name(engine: &Engine) -> Option<&[$type]> { + pub fn $name(engine: &Engine) -> Option<&[$type]> { engine.statistics().$name.as_deref() } )*}; @@ -168,7 +176,7 @@ optional_buffers!(moving_distance: f64, speed: f64, hr: f64, cad: f64, atemp: f6 optional_buffers!(total_time: i64, moving_time: i64); /// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), see `optional_buffers`. -pub fn timestamps(engine: &Engine) -> Option<&[i64]> { +pub fn timestamps(engine: &Engine) -> Option<&[i64]> { engine.statistics().time.as_deref() } @@ -178,17 +186,17 @@ pub fn timestamps(engine: &Engine) -> Option<&[i64]> { // A value that is unknown for every trackpoint is a single interval. macro_rules! categories { ($starts:ident, $values:ident, $names:ident, $field:ident) => { - pub fn $starts(engine: &Engine) -> &[u32] { + pub fn $starts(engine: &Engine) -> &[u32] { &engine.statistics().$field.starts } - pub fn $values(engine: &Engine) -> &[u8] { + pub fn $values(engine: &Engine) -> &[u8] { &engine.statistics().$field.values } /// The names of the category, in the order of their codes. The list only grows: a name /// keeps its index. - pub fn $names(engine: &Engine) -> &[String] { + pub fn $names(engine: &Engine) -> &[String] { engine.categories().$field.names() } }; @@ -203,22 +211,22 @@ categories!(mtb_scale_starts, mtb_scale_values, mtb_scales, mtb_scale); // statistics). The ends of the segments are always anchors. /// The index in the selection of each anchor. -pub fn anchor_indices(engine: &Engine) -> &[u32] { +pub fn anchor_indices(engine: &Engine) -> &[u32] { &engine.routing().anchor_indices } /// The lowest map zoom level at which each anchor is shown. -pub fn anchor_zooms(engine: &Engine) -> &[u8] { +pub fn anchor_zooms(engine: &Engine) -> &[u8] { &engine.routing().anchor_zooms } /// Index in the selection of the first trackpoint of each selected segment that has some. -pub fn segment_starts(engine: &Engine) -> &[u32] { +pub fn segment_starts(engine: &Engine) -> &[u32] { &engine.routing().segment_starts } /// The id of each selected segment that has trackpoints, as `segment_starts`. -pub fn segment_ids(engine: &Engine) -> Vec { +pub fn segment_ids(engine: &Engine) -> Vec { engine .routing() .segment_ids @@ -229,6 +237,6 @@ pub fn segment_ids(engine: &Engine) -> Vec { /// Changes when the selected segments or their trackpoints change: the indices in the selection /// are only valid while it stays the same. -pub fn routing_revision(engine: &Engine) -> u32 { +pub fn routing_revision(engine: &Engine) -> u32 { engine.routing().revision } diff --git a/gpx-rs/src/engine/state/history/global.rs b/gpx-rs/src/engine/state/history/global.rs index c91cabfd5..c56cfdffc 100644 --- a/gpx-rs/src/engine/state/history/global.rs +++ b/gpx-rs/src/engine/state/history/global.rs @@ -57,6 +57,8 @@ impl GlobalHistory { } impl History for GlobalHistory { + const KEEPS_ORIGINALS: bool = false; + fn restored(entry: Files) -> Self { Self { entries: vec![entry], diff --git a/gpx-rs/src/engine/state/history/mod.rs b/gpx-rs/src/engine/state/history/mod.rs index 773c7290a..d8895664d 100644 --- a/gpx-rs/src/engine/state/history/mod.rs +++ b/gpx-rs/src/engine/state/history/mod.rs @@ -26,6 +26,11 @@ pub type Files = HashMap>; /// /// Files are told apart by their identity (`Rc::ptr_eq`), not their content. pub trait History: Default + Debug { + /// Whether the commands that replace files by others (split into files, extract, merge) must + /// keep the files they start from, and produce new ones. A history that forgets a file when it + /// goes could not undo the command, which a history of the whole workspace can. + const KEEPS_ORIGINALS: bool; + /// A history that starts from `files`, which cannot be undone. fn restored(files: Files) -> Self; diff --git a/gpx-rs/src/engine/state/history/per_file.rs b/gpx-rs/src/engine/state/history/per_file.rs index 467519402..b1f6409e0 100644 --- a/gpx-rs/src/engine/state/history/per_file.rs +++ b/gpx-rs/src/engine/state/history/per_file.rs @@ -82,6 +82,8 @@ impl PerFileHistory { } impl History for PerFileHistory { + const KEEPS_ORIGINALS: bool = true; + fn restored(files: Files) -> Self { let stacks = files .iter() diff --git a/tauri/src/commands/edit.rs b/tauri/src/commands/edit.rs index 9e58c9510..b91ee1eb1 100644 --- a/tauri/src/commands/edit.rs +++ b/tauri/src/commands/edit.rs @@ -139,12 +139,12 @@ pub fn create_artificial_timestamps( #[tauri::command(async)] pub fn merge(state: State, merge_type: MergeType, remove_gaps: bool) -> api::Outcome { - state.run(move |e| api::merge(e, merge_type, remove_gaps, false)) + state.run(move |e| api::merge(e, merge_type, remove_gaps)) } #[tauri::command(async)] pub fn extract(state: State) -> api::Outcome { - state.run(|e| api::extract(e, false)) + state.run(api::extract) } #[tauri::command(async)] @@ -214,5 +214,5 @@ pub fn split( at: u32, split_type: SplitType, ) -> api::Outcome { - state.run(move |e| api::split(e, revision, at, split_type, false)) + state.run(move |e| api::split(e, revision, at, split_type)) } diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index d5afcf9cd..6a55ac1eb 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -10,6 +10,10 @@ mod workspace; use commands::*; use tauri::{Emitter, Manager}; +/// The engine of the app: each file has its own history, so that undoing only concerns the +/// selected files, and opening or closing a file is not something to undo. +pub(crate) type Engine = gpx_engine::Engine; + /// The GPX files among what the system gave to the app (the arguments of the command line, when a /// file is opened with it). fn gpx_paths(arguments: impl IntoIterator) -> Vec { diff --git a/tauri/src/state.rs b/tauri/src/state.rs index de2899cd9..681f606f2 100644 --- a/tauri/src/state.rs +++ b/tauri/src/state.rs @@ -11,7 +11,8 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use gpx_engine::{Engine, Persister, Storage, api}; +use crate::Engine; +use gpx_engine::{Persister, Storage, api}; use notify::RecursiveMode; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; use serde::Serialize; diff --git a/tauri/src/store.rs b/tauri/src/store.rs index b3b1b385b..95b8a1ab8 100644 --- a/tauri/src/store.rs +++ b/tauri/src/store.rs @@ -220,7 +220,8 @@ impl Storage for FileStorage { #[cfg(test)] mod tests { - use gpx_engine::{Action, Command, Engine, Load, LoadFiles, Persister}; + use crate::Engine; + use gpx_engine::{Action, Command, Load, LoadFiles, Persister}; use super::*; @@ -255,7 +256,7 @@ mod tests { // another session let storage = FileStorage::open(&root).unwrap(); let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap(); - let mut other: Engine = Engine::default(); + let mut other = Engine::default(); other.restore(restored); assert_eq!(other.order(), order); assert_eq!( diff --git a/tauri/src/tests.rs b/tauri/src/tests.rs index 94f14d4ea..b144bc1a8 100644 --- a/tauri/src/tests.rs +++ b/tauri/src/tests.rs @@ -165,7 +165,8 @@ fn edits_undoes_and_selects() { json!({ "name": "route", "lng": 4.4, "lat": 50.8 }), ); assert_eq!(outcome["changed"], true); - assert_eq!(app.call("can_undo", json!({})), true); + // each file has its own history, which starts when it appears + assert_eq!(app.call("can_undo", json!({})), false); let id = app.call("file_order", json!({}))[0].clone(); let outcome = app.call("metadata", json!({ "name": "renamed", "desc": "" })); @@ -179,6 +180,7 @@ fn edits_undoes_and_selects() { assert_eq!(outcome["selectionChanged"], true); assert_eq!(app.call("selection", json!({})), json!({ "type": "empty" })); app.call("select", json!({ "fileIds": [id], "mode": "toggle" })); + assert_eq!(app.call("can_undo", json!({})), true); app.call("undo", json!({})); assert_eq!( @@ -186,8 +188,11 @@ fn edits_undoes_and_selects() { "route" ); assert_eq!(app.call("can_redo", json!({})), true); - app.call("undo", json!({})); - assert_eq!(app.call("file_order", json!({})), json!([])); + // creating the file is not something to undo + assert_eq!(app.call("can_undo", json!({})), false); + let outcome = app.call("undo", json!({})); + assert_eq!(outcome["changed"], false); + assert_eq!(app.call("file_order", json!({})), json!([id])); // what cannot be done says why let outcome = app.call("reverse", json!({})); diff --git a/tauri/src/workspace.rs b/tauri/src/workspace.rs index 5cfd58e77..1e28bc145 100644 --- a/tauri/src/workspace.rs +++ b/tauri/src/workspace.rs @@ -18,7 +18,8 @@ use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; -use gpx_engine::{Engine, ExportOptions, FileId, api}; +use crate::Engine; +use gpx_engine::{ExportOptions, FileId, api}; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/wasm/src/command/routing.rs b/wasm/src/command/routing.rs index 9ec9dc802..83a32caeb 100644 --- a/wasm/src/command/routing.rs +++ b/wasm/src/command/routing.rs @@ -62,5 +62,5 @@ pub fn change_loop_start(revision: u32, index: u32) -> Outcome { /// nothing, and returns false, if the revision of the routing buffers is not the given one. #[wasm_bindgen] pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome { - run(|e| api::split(e, revision, at, split_type.into(), false)) + run(|e| api::split(e, revision, at, split_type.into())) } diff --git a/wasm/src/command/tools.rs b/wasm/src/command/tools.rs index 9d981c983..55a1ef58c 100644 --- a/wasm/src/command/tools.rs +++ b/wasm/src/command/tools.rs @@ -55,12 +55,12 @@ pub fn create_artificial_timestamps(start_time: f64, total_time: f64) -> Outcome /// that are connected closer in time, if they have timestamps. #[wasm_bindgen] pub fn merge(type_: MergeType, remove_gaps: bool) -> Outcome { - run(|e| api::merge(e, type_.into(), remove_gaps, false)) + run(|e| api::merge(e, type_.into(), remove_gaps)) } #[wasm_bindgen] pub fn extract() -> Outcome { - run(|e| api::extract(e, false)) + run(api::extract) } /// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.