mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
functional undo-redo
This commit is contained in:
16
website/package-lock.json
generated
16
website/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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 -->
|
||||
|
@@ -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();
|
||||
|
@@ -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)
|
||||
|
@@ -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) => {
|
||||
|
@@ -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() {
|
||||
|
Reference in New Issue
Block a user