functional undo-redo

This commit is contained in:
vcoppe
2024-04-30 22:35:54 +02:00
parent 667c94a4c4
commit f24a7ba427
7 changed files with 118 additions and 40 deletions

View File

@@ -13,11 +13,11 @@
"chart.js": "^4.4.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",
"immer": "^10.1.1",
"lucide-svelte": "^0.365.0",
"mapbox-gl": "^3.2.0",
"mode-watcher": "^0.3.0",
"sortablejs": "^1.15.2",
"structurajs": "^0.12.0",
"svelte-i18n": "^4.0.0",
"svelte-sonner": "^0.3.22",
"tailwind-merge": "^2.2.2",
@@ -3357,15 +3357,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5135,6 +5126,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/structurajs": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/structurajs/-/structurajs-0.12.0.tgz",
"integrity": "sha512-vadDl3zCv6OM2dXfzelUH3RLhNu9pmIEZU7zS0jIgMR3BDKIk44fDd/X9KK9LE27MMU+/aw3ibBTQQgBH0zfiw=="
},
"node_modules/subtag": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz",

View File

@@ -46,11 +46,11 @@
"chart.js": "^4.4.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",
"immer": "^10.1.1",
"lucide-svelte": "^0.365.0",
"mapbox-gl": "^3.2.0",
"mode-watcher": "^0.3.0",
"sortablejs": "^1.15.2",
"structurajs": "^0.12.0",
"svelte-i18n": "^4.0.0",
"svelte-sonner": "^0.3.22",
"tailwind-merge": "^2.2.2",

View File

@@ -9,9 +9,9 @@
import { _ } from 'svelte-i18n';
import type { GPXFile } from 'gpx';
import type { Immutable } from 'immer';
import type { FreezedObject } from 'structurajs';
export let file: Writable<Immutable<GPXFile>> | undefined;
export let file: Writable<FreezedObject<GPXFile>> | undefined;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@@ -39,6 +39,10 @@
setMode($settings.mode);
}
}
let undoRedo = filestore.undoRedo;
let undoDisabled = derived(undoRedo, ($undoRedo) => !$undoRedo.canUndo);
let redoDisabled = derived(undoRedo, ($undoRedo) => !$undoRedo.canRedo);
</script>
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
@@ -90,12 +94,12 @@
<Menubar.Menu>
<Menubar.Trigger>{$_('menu.edit')}</Menubar.Trigger>
<Menubar.Content class="border-none">
<Menubar.Item>
<Menubar.Item on:click={$undoRedo.undo} disabled={$undoDisabled}>
<Undo2 size="16" class="mr-1" />
{$_('menu.undo')}
<Shortcut key="Z" ctrl={true} />
</Menubar.Item>
<Menubar.Item>
<Menubar.Item on:click={$undoRedo.redo} disabled={$redoDisabled}>
<Redo2 size="16" class="mr-1" />
{$_('menu.redo')}
<Shortcut key="Z" ctrl={true} shift={true} />
@@ -211,6 +215,12 @@
exportSelectedFiles();
}
e.preventDefault();
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) {
$undoRedo.redo();
} else {
$undoRedo.undo();
}
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) {
filestore.deleteAllFiles();

View File

@@ -26,13 +26,19 @@
// remove controls for deleted files
routingControls.forEach((controls, fileId) => {
if (!get(filestore).find((file) => file._data.id === fileId)) {
console.log('remove controls for deleted file', fileId);
controls.remove();
routingControls.delete(fileId);
if (selectedId === fileId) {
selectedId = null;
}
}
});
}
$: if ($map && $selectedFiles) {
console.log('selectedFiles', $selectedFiles);
// update selected file
if ($selectedFiles.size == 0 || $selectedFiles.size > 1 || !active) {
if (selectedId) {
@@ -48,12 +54,14 @@
selectedId = newSelectedId;
}
}
console.log('selectedId', selectedId);
}
$: if ($map && selectedId) {
if (!routingControls.has(selectedId)) {
let selectedFileStore = filestore.getFileStore(selectedId);
if (selectedFileStore) {
console.log('add controls for selected file', selectedId);
routingControls.set(
selectedId,
new RoutingControls(get(map), selectedFileStore, popup, popupElement)

View File

@@ -1,12 +1,17 @@
import { writable, get, type Readable, type Writable } from "svelte/store";
import { GPXFile } from "gpx";
import { produceWithPatches, enableMapSet, enablePatches, type Immutable } from "immer";
import { produceWithPatches, type FreezedObject, type UnFreezedObject, applyPatches, type Patch } from "structurajs";
import { fileOrder, selectedFiles } from "./stores";
enableMapSet();
enablePatches();
export type UndoRedoStore = {
canUndo: boolean;
canRedo: boolean;
undo: () => void;
redo: () => void;
}
export type GPXFileStore = Readable<GPXFile[]> & {
undoRedo: Writable<UndoRedoStore>;
add: (file: GPXFile) => void;
addMultiple: (files: GPXFile[]) => void;
applyToFile: (id: string, callback: (file: GPXFile) => void) => void;
@@ -14,17 +19,13 @@ export type GPXFileStore = Readable<GPXFile[]> & {
duplicateSelectedFiles: () => void;
deleteSelectedFiles: () => void;
deleteAllFiles: () => void;
getFileStore: (id: string) => Writable<Immutable<GPXFile>> | undefined;
getFileStore: (id: string) => Writable<FreezedObject<GPXFile>> | undefined;
}
export function createGPXFileStore(): GPXFileStore {
let files: Immutable<Map<string, GPXFile>> = new Map();
let files: ReadonlyMap<string, FreezedObject<GPXFile>> = new Map();
let subscribers: Set<Function> = new Set();
let filestores = new Map<string, Writable<Immutable<GPXFile>>>();
let patches = [];
function notifySubscriber(run: Function) {
run(Array.from(files.values()));
}
@@ -35,33 +36,79 @@ export function createGPXFileStore(): GPXFileStore {
});
}
let filestores = new Map<string, Writable<FreezedObject<GPXFile>>>();
let patches: { patch: Patch[], inversePatch: Patch[], global: boolean }[] = [];
let patchIndex = -1;
function updateUndoRedo() {
undoRedo.update($undoRedo => {
$undoRedo.canUndo = patchIndex >= 0;
$undoRedo.canRedo = patchIndex < patches.length - 1;
return $undoRedo;
});
}
function appendPatches(patch: Patch[], inversePatch: Patch[], global: boolean) {
patches = patches.slice(0, patchIndex + 1);
patches.push({
patch,
inversePatch,
global
});
patchIndex++;
updateUndoRedo();
}
let undoRedo: Writable<UndoRedoStore> = writable({
canUndo: false,
canRedo: false,
undo: () => {
if (patchIndex >= 0) {
applyPatch(patches[patchIndex].inversePatch, patches[patchIndex].global);
patchIndex--;
}
},
redo: () => {
if (patchIndex < patches.length - 1) {
patchIndex++;
applyPatch(patches[patchIndex].patch, patches[patchIndex].global);
}
},
});
function applyPatch(patch: Patch[], global: boolean) {
files = applyPatches(files, patch);
for (let p of patch) {
let fileId = p.p?.toString();
if (fileId) {
let filestore = filestores.get(fileId), newFile = files.get(fileId);
if (filestore && newFile) {
filestore.set(newFile);
}
}
}
if (global) {
console.log("Global patch", patch);
notify();
}
updateUndoRedo();
}
function applyToGlobalStore(callback: (files: Map<string, GPXFile>) => void) {
const [newFiles, patch, inversePatch] = produceWithPatches(files, callback);
files = newFiles;
patches.push({
patch,
inversePatch,
global: true
});
console.log(patches[patches.length - 1]);
appendPatches(patch, inversePatch, true);
notify();
}
function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) {
function applyToFiles(fileIds: string[], callback: (file: UnFreezedObject<FreezedObject<GPXFile>>) => void) {
const [newFiles, patch, inversePatch] = produceWithPatches(files, (draft) => {
fileIds.forEach((fileId) => {
if (draft.has(fileId)) {
callback(draft.get(fileId));
}
callback(draft.get(fileId));
});
});
files = newFiles;
patches.push({
patch,
inversePatch,
global: false
});
console.log(patches[patches.length - 1]);
appendPatches(patch, inversePatch, false);
fileIds.forEach((fileId) => {
let filestore = filestores.get(fileId), newFile = newFiles.get(fileId);
if (filestore && newFile) {
@@ -93,6 +140,7 @@ export function createGPXFileStore(): GPXFileStore {
subscribers.delete(run);
}
},
undoRedo,
add: (file: GPXFile) => {
file._data.id = getLayerId();
applyToGlobalStore((draft) => {

View File

@@ -14,6 +14,22 @@ export const fileOrder = writable<string[]>([]);
export const selectedFiles = writable<Set<string>>(new Set());
export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({});
filestore.subscribe((files) => { // Update selectedFiles if a file is deleted
let deletedFileIds: string[] = [];
get(selectedFiles).forEach((fileId) => {
if (!files.find((f) => f._data.id === fileId)) {
deletedFileIds.push(fileId);
}
});
if (deletedFileIds.length > 0) {
selectedFiles.update((selectedFiles) => {
deletedFileIds.forEach((fileId) => selectedFiles.delete(fileId));
return selectedFiles;
});
}
});
export const gpxData = writable(new GPXFiles([]).getTrackPointsAndStatistics());
function updateGPXData() {