From 764f9fe5a450744ef8c34ed07e79c75e86967518 Mon Sep 17 00:00:00 2001
From: MassiveBox <box@massivebox.net>
Date: Tue, 6 May 2025 18:19:18 +0200
Subject: [PATCH] Add option to remember editor position and zoom

---
 public/i18n/en_US.json |  4 ++++
 src/config.ts          | 15 ++++++++++++++-
 src/editor.ts          | 35 ++++++++++++++++++++++++++++++++---
 3 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json
index aa868b1..fbeb088 100644
--- a/public/i18n/en_US.json
+++ b/public/i18n/en_US.json
@@ -35,6 +35,10 @@
     "analytics": {
       "title": "Analytics",
       "description": "Enable to send anonymous usage data to the developer. <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy Policy</a>"
+    },
+    "restorePosition": {
+      "title": "Remember editor position",
+      "description": "When enabled, the editor will remember the zoom factor and position, and it will restore them the next time you open the drawing."
     }
   }
 }
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index dd1c3ea..11f5c2b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -4,6 +4,7 @@ import {Plugin, showMessage} from "siyuan";
 import {SettingUtils} from "@/libs/setting-utils";
 
 type Options = {
+    restorePosition: boolean;
     grid: boolean
     background: string
     dialogOnDesktop: boolean
@@ -11,6 +12,7 @@ type Options = {
 };
 
 export type DefaultEditorOptions = {
+    restorePosition: boolean;
     grid: boolean
     background: string
 }
@@ -30,8 +32,9 @@ export class PluginConfig {
 
     getDefaultEditorOptions(): DefaultEditorOptions {
         return {
+            restorePosition: this.options.restorePosition,
             grid: this.options.grid,
-            background: this.options.background,
+            background: this.options.background
         };
     }
 
@@ -50,6 +53,7 @@ export class PluginConfig {
             background: "#00000000",
             dialogOnDesktop: false,
             analytics: true,
+            restorePosition: true,
         };
         this.firstRun = true;
     }
@@ -107,6 +111,7 @@ export class PluginConfigViewer {
             background: color,
             dialogOnDesktop: data.dialogOnDesktop,
             analytics: data.analytics,
+            restorePosition: data.restorePosition,
         });
         await this.config.save();
 
@@ -148,6 +153,14 @@ export class PluginConfigViewer {
             type: 'textinput',
         });
 
+        this.settingUtils.addItem({
+            key: "restorePosition",
+            title: this.plugin.i18n.settings.restorePosition.title,
+            description: this.plugin.i18n.settings.restorePosition.description,
+            value: this.config.options.restorePosition,
+            type: 'checkbox'
+        });
+
         this.settingUtils.addItem({
             key: "dialogOnDesktop",
             title: this.plugin.i18n.settings.dialogOnDesktop.title,
diff --git a/src/editor.ts b/src/editor.ts
index 804a1e1..cba875d 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -1,7 +1,15 @@
 import {MaterialIconProvider} from "@js-draw/material-icons";
 import {PluginAsset, PluginFile} from "@/file";
 import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const";
-import Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw";
+import Editor, {
+    BackgroundComponentBackgroundType,
+    BaseWidget,
+    Color4,
+    EditorEventType,
+    Mat33,
+    Vec2,
+    Viewport
+} from "js-draw";
 import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan";
 import {findSyncIDInProtyle, replaceSyncID} from "@/protyle";
 import DrawJSPlugin from "@/index";
@@ -62,9 +70,26 @@ export class PluginEditor {
 
         this.drawingFile = new PluginAsset(this.fileID, this.syncID, SVG_MIME);
         await this.drawingFile.loadFromSiYuanFS();
+        const drawingContent = this.drawingFile.getContent();
+
+        if(drawingContent != null) {
+
+            await this.editor.loadFromSVG(drawingContent);
+
+            // restore position and zoom
+            const svgElem = new DOMParser().parseFromString(drawingContent, SVG_MIME).documentElement;
+            const editorViewStr = svgElem.getAttribute('editorView');
+            if(editorViewStr != null && defaultEditorOptions.restorePosition) {
+                try {
+                    const [viewBoxOriginX, viewBoxOriginY, zoom] = editorViewStr.split(' ').map(x => parseFloat(x));
+                    this.editor.dispatch(Viewport.transformBy(Mat33.scaling2D(zoom)));
+                    this.editor.dispatch(Viewport.transformBy(Mat33.translation(Vec2.of(
+                        - viewBoxOriginX,
+                        - viewBoxOriginY
+                    ))));
+                }catch (e){}
+            }
 
-        if(this.drawingFile.getContent() != null) {
-            await this.editor.loadFromSVG(this.drawingFile.getContent());
         }else{
             // it's a new drawing
             this.editor.dispatch(this.editor.setBackgroundStyle({
@@ -106,6 +131,10 @@ export class PluginEditor {
         let newSyncID: string;
         const oldSyncID = this.syncID;
 
+        const rect = this.editor.viewport.visibleRect;
+        const zoom = this.editor.viewport.getScaleFactor();
+        svgElem.setAttribute('editorView', `${rect.x} ${rect.y} ${zoom}`)
+
         try {
             this.drawingFile.setContent(svgElem.outerHTML);
             await this.drawingFile.save();