mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
2 Commits
1.3.16
...
ImageEleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78fb37b173 | ||
|
|
a17638717f |
@@ -186,7 +186,7 @@ Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||
- true: the drawing will be saved after the elements were added.
|
||||
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||
|
||||
#### onDropHook
|
||||
### onDropHook
|
||||
```typescript
|
||||
onDropHook (data: {
|
||||
ea: ExcalidrawAutomate,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "0.9.0-obsidian-11",
|
||||
"@zsviczian/excalidraw": "0.9.0-obsidian-image-support-3",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_parse,
|
||||
VIEW_TYPE_EXCALIDRAW
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
MAX_IMAGE_SIZE
|
||||
} from "./constants";
|
||||
import { wrapText } from "./Utils";
|
||||
import { getObsidianImage, wrapText } from "./Utils";
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
@@ -26,6 +27,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {};
|
||||
imagesDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
@@ -102,6 +104,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
endObjectId?:string
|
||||
}
|
||||
):string ;
|
||||
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
|
||||
connectObjects (
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint,
|
||||
@@ -160,6 +163,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementsDict: {},
|
||||
imagesDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
@@ -513,6 +517,25 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
}
|
||||
return id;
|
||||
},
|
||||
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
|
||||
const id = nanoid();
|
||||
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||
if(!image) return null;
|
||||
this.imagesDict[image.imageId] = {
|
||||
type:"image",
|
||||
id: image.imageId,
|
||||
dataURL: image.dataURL
|
||||
}
|
||||
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||
image.size.width = scale*image.size.width;
|
||||
image.size.height = scale*image.size.height;
|
||||
}
|
||||
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||
this.elementsDict[id].imageId = image.imageId;
|
||||
this.elementsDict[id].scale = [1,1];
|
||||
return id;
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
@@ -552,6 +575,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
},
|
||||
clear() {
|
||||
this.elementsDict = {};
|
||||
this.imagesDict = {};
|
||||
},
|
||||
reset() {
|
||||
this.clear();
|
||||
@@ -682,7 +706,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
return false;
|
||||
}
|
||||
const elements = this.getElements();
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save);
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||
},
|
||||
onDropHook:null,
|
||||
};
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
FULLSCREEN_ICON_NAME,
|
||||
JSON_parse
|
||||
JSON_parse,
|
||||
IMAGE_TYPES
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
||||
@@ -459,6 +460,12 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
setMarkdownView() {
|
||||
if(this.excalidrawRef) {
|
||||
const el = this.excalidrawRef.current.getSceneElements();
|
||||
if(el.filter((e:any)=>e.type==="image").length>0) {
|
||||
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
||||
}
|
||||
}
|
||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||
this.plugin.setMarkdownView(this.leaf);
|
||||
}
|
||||
@@ -640,7 +647,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
||||
}
|
||||
|
||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> => {
|
||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
|
||||
if(!excalidrawRef?.current) return false;
|
||||
|
||||
const textElements = newElements.filter((el)=>el.type=="text");
|
||||
@@ -653,7 +660,20 @@ export default class ExcalidrawView extends TextFileView {
|
||||
};
|
||||
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
let st: AppState = excalidrawRef.current.getAppState();
|
||||
if(!st.files) {
|
||||
st.files = {};
|
||||
}
|
||||
if(images) {
|
||||
Object.keys(images).forEach((k)=>{
|
||||
st.files[k]={
|
||||
type:images[k].type,
|
||||
id: images[k].id,
|
||||
dataURL: images[k].dataURL
|
||||
}
|
||||
});
|
||||
}
|
||||
//merge appstate.files with files
|
||||
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: el.concat(newElements),
|
||||
@@ -670,6 +690,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
|
||||
if(st.files) {
|
||||
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.imageId);
|
||||
const toDelete = Object.keys(st.files).filter((k)=>!imgIds.contains(k));
|
||||
toDelete.forEach((k)=>delete st.files[k]);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -693,6 +720,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||
gridSize: st.gridSize,
|
||||
files: st.files??{},
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -947,6 +975,19 @@ export default class ExcalidrawView extends TextFileView {
|
||||
switch(draggable?.type) {
|
||||
case "file":
|
||||
if (!onDropHook("file",[draggable.file],null)) {
|
||||
if((event.ctrlKey || event.metaKey) && IMAGE_TYPES.contains(draggable.file.extension)) {
|
||||
const f = draggable.file;
|
||||
const topX = currentPosition.x;
|
||||
const topY = currentPosition.y;
|
||||
const ea = window.ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setView(this);
|
||||
(async () => {
|
||||
await ea.addImage(currentPosition.x,currentPosition.y,draggable.file);
|
||||
ea.addElementsToView(false,false);
|
||||
})();
|
||||
return false;
|
||||
}
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
||||
}
|
||||
return false;
|
||||
|
||||
69
src/Utils.ts
69
src/Utils.ts
@@ -1,6 +1,9 @@
|
||||
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { IMAGE_TYPES } from "./constants";
|
||||
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
@@ -122,4 +125,66 @@ export const viewportCoordsToSceneCoords = (
|
||||
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||
return { x, y };
|
||||
};
|
||||
};
|
||||
|
||||
export const getObsidianImage = async (app: App, file: TFile)
|
||||
:Promise<{
|
||||
imageId: string,
|
||||
dataURL: string,
|
||||
size: {height: number, width: number},
|
||||
}> => {
|
||||
if(!app || !file) return null;
|
||||
if (!IMAGE_TYPES.contains(file.extension)) return null;
|
||||
const ab = await app.vault.readBinary(file);
|
||||
return {
|
||||
imageId: await generateIdFromFile(ab),
|
||||
dataURL: file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab),
|
||||
size: await getImageSize(app,file)
|
||||
}
|
||||
}
|
||||
|
||||
const getSVGData = async (app: App, file: TFile): Promise<string> => {
|
||||
const svg = await app.vault.read(file);
|
||||
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))))
|
||||
}
|
||||
|
||||
const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataURL = reader.result as string;
|
||||
resolve(dataURL);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
|
||||
});
|
||||
};
|
||||
|
||||
const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
||||
let id: string;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest(
|
||||
"SHA-1",
|
||||
file,
|
||||
);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
id = nanoid(40);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const getImageSize = async (app: App, file:TFile):Promise<{height:number, width:number}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let img = new Image()
|
||||
img.onload = () => resolve({height: img.height, width:img.width});
|
||||
img.onerror = reject;
|
||||
img.src = app.vault.getResourcePath(file);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("["
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
|
||||
export const MAX_IMAGE_SIZE = 600;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
|
||||
@@ -44,6 +44,8 @@ export default {
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||
"loading Markdown View may take a while. Please be patient. ",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
|
||||
@@ -1024,10 +1024,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@zsviczian/excalidraw@0.9.0-obsidian-11":
|
||||
"integrity" "sha512-h4d8l0slwWB2yLaZnD1qSoYQ9eaZFWEe2Ls2rJYNIc6v9EAEAWMj/4NGRgOpcxdU4dKt5MSknHzezRsfeDz5bQ=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-11.tgz"
|
||||
"version" "0.9.0-obsidian-11"
|
||||
"@zsviczian/excalidraw@0.9.0-obsidian-image-support-3":
|
||||
"integrity" "sha512-Y+hIhIxoNsoyYS2AmhE9I8G0M6Q6KaQ3LxSn8p3JZbUGFqoAmDg+26fGHXhMsboaVXhKqonkw5UN1UlZYL5k1A=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-image-support-3.tgz"
|
||||
"version" "0.9.0-obsidian-image-support-3"
|
||||
|
||||
"abab@^1.0.3":
|
||||
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
|
||||
|
||||
Reference in New Issue
Block a user