mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 225c6305d1 | |||
| ba9ab61cc9 | |||
| 0940a8628a | |||
| 46ee9e9524 | |||
| c044278a4a | |||
| aa9118cdae | |||
| d19b32d0c4 | |||
| dd7f0750fd | |||
| e1330cd8bb | |||
| 04367bd3cd | |||
| 7d139462bf | |||
| d8e429d815 | |||
| 4685a6f014 | |||
| 03b389a2b5 | |||
| 024f7979a7 | |||
| f9e9ac0728 | |||
| c6eb5859b5 | |||
| 377268f30c |
@@ -16,3 +16,4 @@ lib
|
|||||||
|
|
||||||
#VSCode
|
#VSCode
|
||||||
.vscode
|
.vscode
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
|||||||
|[](https://youtu.be/eKFmrSQhFA4)|[](https://youtu.be/qbPIAZguJeo)|[](https://youtu.be/2Y8OhkGiTHg)|
|
|[](https://youtu.be/eKFmrSQhFA4)|[](https://youtu.be/qbPIAZguJeo)|[](https://youtu.be/2Y8OhkGiTHg)|
|
||||||
|[](https://youtu.be/2v9TZmQNO8c)|[](https://youtu.be/xHPGWR3m0c8)|[](https://youtu.be/gMIKXyhS-dM)|
|
|[](https://youtu.be/2v9TZmQNO8c)|[](https://youtu.be/xHPGWR3m0c8)|[](https://youtu.be/gMIKXyhS-dM)|
|
||||||
|[](https://youtu.be/Etskjw7a5zo)|[](https://youtu.be/4N6efq1DtH0)|[](https://youtu.be/U2LkBRBk4LY)|
|
|[](https://youtu.be/Etskjw7a5zo)|[](https://youtu.be/4N6efq1DtH0)|[](https://youtu.be/U2LkBRBk4LY)|
|
||||||
| [](https://youtu.be/qiKuqMcNWgU)|[](https://youtu.be/yZQoJg2RCKI)| |
|
| [](https://youtu.be/qiKuqMcNWgU)|[](https://youtu.be/yZQoJg2RCKI)|[](https://youtu.be/6PLGHBH9VZ4) |
|
||||||
|
|[](https://youtu.be/epYNx2FSf2w) | [](https://youtu.be/Amhlv6r9WvM) | [](https://youtu.be/r9oB1SlK1GU) |
|
||||||
|
|[](https://youtu.be/7gJDwNgQ6NU) | | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Key features
|
# Key features
|
||||||
@@ -50,8 +53,8 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
|||||||
- Insert LaTeX formulas using the Command Palette action "Insert LaTeX formula". You can edit formulas either in Markdown view, or by <kbd>CTRL/CMD + Click</kbd> on the formula.
|
- Insert LaTeX formulas using the Command Palette action "Insert LaTeX formula". You can edit formulas either in Markdown view, or by <kbd>CTRL/CMD + Click</kbd> on the formula.
|
||||||
- Drag & Drop support
|
- Drag & Drop support
|
||||||
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw.
|
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw.
|
||||||
- Dragging image files (PNG, SVG, JPG, Excalidraw) from Obsidian's file explorer while pressing the <kbd>CTRL/CMD</kbd> button will embed the image into your drawing.
|
- Dragging image files (PNG, SVG, JPG, ICO, GIF, WEBP, Excalidraw) from Obsidian's file explorer while pressing the <kbd>CTRL</kbd> (<kbd>SHIFT</kbd> on Mac) button will embed the image into your drawing.
|
||||||
- You can drag and drop images from outside Obsidian onto Excalidraw. These images will be embedded into your drawing and saved to Obsidian.
|
- If in addition to <kbd>CTRL</kbd> or <kbd>SHIFT</kbd> you also hold down <kbd>ALT<kbd>, the image will be inserted at 100% of its size. ⚠ Note: this is a very niche feature with a very particular behavior that I built primarily for myself (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉)... This will reset your embedded image to 100% size every time you open the Excalidraw drawing, or in case you have embedded an Excalidraw drawing on your canvas inserted using this function, every time you update the embedded drawing, it will be scaled back to 100% size. This means that even if you resize the image on the drawing, it will reset to 100% the next time you open the file or you modify the original embedded object. This feature is useful when you decompose a drawing into separate Excalidraw files, but when combined onto a single canvas you want the individual pieces to maintain their actual sizes. I use this feature to construct Book-on-a-Page summaries from atomic drawings.
|
||||||
- You can drag and drop text from Markdown views onto Excalidraw.
|
- You can drag and drop text from Markdown views onto Excalidraw.
|
||||||
- You can drag and drop web addresses from your browser and they will become links.
|
- You can drag and drop web addresses from your browser and they will become links.
|
||||||
- Image support
|
- Image support
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||||
* This callback must return a boolean value.
|
* This callback must return a boolean value.
|
||||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
* In case you want to prevent the excalidraw onLinkHover action you must return true, it will stop the native excalidraw onLinkHover management flow.
|
||||||
*/
|
*/
|
||||||
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-excalidraw-plugin",
|
"id": "obsidian-excalidraw-plugin",
|
||||||
"name": "Excalidraw",
|
"name": "Excalidraw",
|
||||||
"version": "1.7.22",
|
"version": "1.7.26",
|
||||||
"minAppVersion": "0.15.6",
|
"minAppVersion": "0.15.6",
|
||||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||||
"author": "Zsolt Viczian",
|
"author": "Zsolt Viczian",
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-excalidraw-plugin",
|
"name": "obsidian-excalidraw-plugin",
|
||||||
"version": "1.7.11",
|
"version": "1.7.26",
|
||||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lz-string": "^1.3.34",
|
"@types/lz-string": "^1.3.34",
|
||||||
"@zsviczian/excalidraw": "0.12.0-obsidian-9",
|
"@zsviczian/excalidraw": "0.13.0-obsidian",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"monkey-around": "^2.3.0",
|
"monkey-around": "^2.3.0",
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"roughjs": "^4.5.2",
|
"roughjs": "^4.5.2",
|
||||||
"colormaster": "1.2.1"
|
"colormaster": "1.2.1",
|
||||||
|
"chroma-js": "^2.4.2",
|
||||||
|
"gl-matrix": "^3.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.12",
|
"@babel/core": "^7.16.12",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"@rollup/plugin-replace": "^3.0.1",
|
"@rollup/plugin-replace": "^3.0.1",
|
||||||
"@rollup/plugin-typescript": "^8.3.0",
|
"@rollup/plugin-typescript": "^8.3.0",
|
||||||
"@types/js-beautify": "^1.13.3",
|
"@types/js-beautify": "^1.13.3",
|
||||||
|
"@types/chroma-js": "^2.1.4",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||||
@@ -49,7 +52,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"html2canvas": "^1.4.0",
|
"html2canvas": "^1.4.0",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"obsidian": "^0.15.4",
|
"obsidian": "^0.16.3",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"rollup": "^2.70.1",
|
"rollup": "^2.70.1",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
|
|||||||
@@ -41,10 +41,14 @@ export declare type MimeType =
|
|||||||
| "image/png"
|
| "image/png"
|
||||||
| "image/jpeg"
|
| "image/jpeg"
|
||||||
| "image/gif"
|
| "image/gif"
|
||||||
|
| "image/webp"
|
||||||
|
| "image/bmp"
|
||||||
|
| "image/x-icon"
|
||||||
| "application/octet-stream";
|
| "application/octet-stream";
|
||||||
export type FileData = BinaryFileData & {
|
export type FileData = BinaryFileData & {
|
||||||
size: Size;
|
size: Size;
|
||||||
hasSVGwithBitmap: boolean;
|
hasSVGwithBitmap: boolean;
|
||||||
|
shouldScale: boolean; //true if image should maintain its area, false if image should display at 100% its size
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Size = {
|
export type Size = {
|
||||||
@@ -179,6 +183,14 @@ export class EmbeddedFile {
|
|||||||
}
|
}
|
||||||
return this.img; //images that are not SVGwithBitmap, only the light string is stored, since inverted and non-inverted are ===
|
return this.img; //images that are not SVGwithBitmap, only the light string is stored, since inverted and non-inverted are ===
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100%
|
||||||
|
*/
|
||||||
|
public shouldScale() {
|
||||||
|
return !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmbeddedFilesLoader {
|
export class EmbeddedFilesLoader {
|
||||||
@@ -294,6 +306,15 @@ export class EmbeddedFilesLoader {
|
|||||||
case "gif":
|
case "gif":
|
||||||
mimeType = "image/gif";
|
mimeType = "image/gif";
|
||||||
break;
|
break;
|
||||||
|
case "webp":
|
||||||
|
mimeType = "image/webp";
|
||||||
|
break;
|
||||||
|
case "bmp":
|
||||||
|
mimeType = "image/bmp";
|
||||||
|
break;
|
||||||
|
case "ico":
|
||||||
|
mimeType = "image/x-icon"
|
||||||
|
break;
|
||||||
case "svg":
|
case "svg":
|
||||||
case "md":
|
case "md":
|
||||||
mimeType = "image/svg+xml";
|
mimeType = "image/svg+xml";
|
||||||
@@ -355,6 +376,7 @@ export class EmbeddedFilesLoader {
|
|||||||
created: data.created,
|
created: data.created,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
hasSVGwithBitmap: data.hasSVGwithBitmap,
|
hasSVGwithBitmap: data.hasSVGwithBitmap,
|
||||||
|
shouldScale: embeddedFile.shouldScale()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (embeddedFile.isSVGwithBitmap) {
|
} else if (embeddedFile.isSVGwithBitmap) {
|
||||||
@@ -365,6 +387,7 @@ export class EmbeddedFilesLoader {
|
|||||||
created: embeddedFile.mtime,
|
created: embeddedFile.mtime,
|
||||||
size: embeddedFile.size,
|
size: embeddedFile.size,
|
||||||
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
|
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
|
||||||
|
shouldScale: embeddedFile.shouldScale()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,6 +406,7 @@ export class EmbeddedFilesLoader {
|
|||||||
created: data.created,
|
created: data.created,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
hasSVGwithBitmap: false,
|
hasSVGwithBitmap: false,
|
||||||
|
shouldScale: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
} from "@zsviczian/excalidraw/types/element/types";
|
} from "@zsviczian/excalidraw/types/element/types";
|
||||||
import { normalizePath, TFile, WorkspaceLeaf } from "obsidian";
|
import { normalizePath, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||||
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
|
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
|
||||||
import { ExcalidrawData } from "./ExcalidrawData";
|
import { ExcalidrawData } from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
embedFontsInSVG,
|
embedFontsInSVG,
|
||||||
errorlog,
|
errorlog,
|
||||||
getEmbeddedFilenameParts,
|
getEmbeddedFilenameParts,
|
||||||
|
getImageSize,
|
||||||
getPNG,
|
getPNG,
|
||||||
getSVG,
|
getSVG,
|
||||||
isVersionNewerThanOther,
|
isVersionNewerThanOther,
|
||||||
@@ -56,6 +58,7 @@ import HSVPlugin from "colormaster/plugins/hsv";
|
|||||||
import RYBPlugin from "colormaster/plugins/ryb";
|
import RYBPlugin from "colormaster/plugins/ryb";
|
||||||
import CMYKPlugin from "colormaster/plugins/cmyk";
|
import CMYKPlugin from "colormaster/plugins/cmyk";
|
||||||
import { TInput } from "colormaster/types";
|
import { TInput } from "colormaster/types";
|
||||||
|
import {ConversionResult, svgToExcalidraw} from "./svgToExcalidraw/parser"
|
||||||
|
|
||||||
extendPlugins([
|
extendPlugins([
|
||||||
HarmonyPlugin,
|
HarmonyPlugin,
|
||||||
@@ -891,6 +894,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
topX: number,
|
topX: number,
|
||||||
topY: number,
|
topY: number,
|
||||||
imageFile: TFile,
|
imageFile: TFile,
|
||||||
|
scale: boolean = true, //true will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const loader = new EmbeddedFilesLoader(
|
const loader = new EmbeddedFilesLoader(
|
||||||
@@ -907,11 +911,11 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
id: fileId,
|
id: fileId,
|
||||||
dataURL: image.dataURL,
|
dataURL: image.dataURL,
|
||||||
created: image.created,
|
created: image.created,
|
||||||
file: imageFile.path,
|
file: imageFile.path + (scale ? "":"|100%"),
|
||||||
hasSVGwithBitmap: image.hasSVGwithBitmap,
|
hasSVGwithBitmap: image.hasSVGwithBitmap,
|
||||||
latex: null,
|
latex: null,
|
||||||
};
|
};
|
||||||
if (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE) {
|
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
|
||||||
const scale =
|
const scale =
|
||||||
MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height);
|
MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height);
|
||||||
image.size.width = scale * image.size.width;
|
image.size.width = scale * image.size.width;
|
||||||
@@ -1214,7 +1218,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
getViewElements(): ExcalidrawElement[] {
|
getViewElements(): ExcalidrawElement[] {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (!this.targetView || !this.targetView?._loaded) {
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
errorMessage("targetView not set", "getViewElements()");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const current = this.targetView?.excalidrawRef?.current;
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
@@ -1232,7 +1236,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean {
|
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (!this.targetView || !this.targetView?._loaded) {
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
errorMessage("targetView not set", "deleteViewElements()");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const current = this.targetView?.excalidrawRef?.current;
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
@@ -1280,7 +1284,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
|
getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (!this.targetView || !this.targetView?._loaded) {
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
errorMessage("targetView not set", "getViewFileForImageElement()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!el || el.type !== "image") {
|
if (!el || el.type !== "image") {
|
||||||
@@ -1488,6 +1492,15 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||||
}) => boolean = null;
|
}) => boolean = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, this callback is triggered whenever the active canvas color changes
|
||||||
|
*/
|
||||||
|
onCanvasColorChangeHook: (
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
view: ExcalidrawView, //the excalidraw view
|
||||||
|
color: string,
|
||||||
|
) => void = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function to generate EmbeddedFilesLoader object
|
* utility function to generate EmbeddedFilesLoader object
|
||||||
* @param isDark
|
* @param isDark
|
||||||
@@ -1678,6 +1691,30 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
return { width: size.w ?? 0, height: size.h ?? 0 };
|
return { width: size.w ?? 0, height: size.h ?? 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the image element at 100% (i.e. the original size)
|
||||||
|
* @param imageElement an image element from the active scene on targetView
|
||||||
|
*/
|
||||||
|
async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> {
|
||||||
|
//@ts-ignore
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getOriginalImageSize()");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(!imageElement || imageElement.type !== "image") {
|
||||||
|
errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ef = this.targetView.excalidrawData.getFile(imageElement.fileId);
|
||||||
|
if(!ef) {
|
||||||
|
errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
|
||||||
|
const dataURL = ef.getImage(isDark);
|
||||||
|
return await getImageSize(dataURL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||||
* recommended use:
|
* recommended use:
|
||||||
@@ -1831,8 +1868,22 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation.");
|
log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(typeof color === "string") {
|
||||||
|
color = this.colorNameToHex(color);
|
||||||
|
}
|
||||||
|
|
||||||
return CM(color);
|
return CM(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importSVG(svgString:string):boolean {
|
||||||
|
const res:ConversionResult = svgToExcalidraw(svgString);
|
||||||
|
if(res.hasErrors) {
|
||||||
|
new Notice (`There were errors while parsing the given SVG:\n${[...res.errors].map((el) => el.innerHTML)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.copyViewElementsToEAforEditing(res.content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function initExcalidrawAutomate(
|
export async function initExcalidrawAutomate(
|
||||||
|
|||||||
+37
-10
@@ -488,7 +488,7 @@ export class ExcalidrawData {
|
|||||||
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||||
let parts;
|
let parts;
|
||||||
while (!(parts = res.next()).done) {
|
while (!(parts = res.next()).done) {
|
||||||
const text = data.substring(position, parts.value.index);
|
let text = data.substring(position, parts.value.index);
|
||||||
const id: string = parts.value[1];
|
const id: string = parts.value[1];
|
||||||
const textEl = this.scene.elements.filter((el: any) => el.id === id)[0];
|
const textEl = this.scene.elements.filter((el: any) => el.id === id)[0];
|
||||||
if (textEl) {
|
if (textEl) {
|
||||||
@@ -502,6 +502,13 @@ export class ExcalidrawData {
|
|||||||
this.elementLinks.set(id, text);
|
this.elementLinks.set(id, text);
|
||||||
} else {
|
} else {
|
||||||
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
|
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
|
||||||
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
|
||||||
|
const elementLinkRes = text.matchAll(/^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm);
|
||||||
|
const elementLink = elementLinkRes.next();
|
||||||
|
if(!elementLink.done) {
|
||||||
|
text = text.replace(/^%%\*\*\*>>>text element-link:\[\[[^<*\]]*]]<<<\*\*\*%%/gm,"");
|
||||||
|
textEl.link = elementLink.value[1];
|
||||||
|
}
|
||||||
const parseRes = await this.parse(text);
|
const parseRes = await this.parse(text);
|
||||||
this.textElements.set(id, {
|
this.textElements.set(id, {
|
||||||
raw: text,
|
raw: text,
|
||||||
@@ -870,8 +877,14 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
if (REGEX_LINK.isTransclusion(parts)) {
|
if (REGEX_LINK.isTransclusion(parts)) {
|
||||||
//transclusion //parts.value[1] || parts.value[4]
|
//transclusion //parts.value[1] || parts.value[4]
|
||||||
const contents = this.parseCheckbox((await this.getTransclusion(REGEX_LINK.getLink(parts)))
|
let contents = this
|
||||||
.contents);
|
.parseCheckbox((await this.getTransclusion(REGEX_LINK.getLink(parts))).contents)
|
||||||
|
.replaceAll(/%%[^%]*%%/gm,""); //remove comments, consequence of https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
|
||||||
|
if(this.plugin.settings.removeTransclusionQuoteSigns) {
|
||||||
|
//remove leading > signs from transcluded quotations; the first > sign is not explicitlyl removed becuse
|
||||||
|
//Obsidian app.metadataCache.blockCache returns the block position already discarding the first '> '
|
||||||
|
contents = contents.replaceAll(/\n\s*>\s?/gm,"\n");
|
||||||
|
}
|
||||||
outString +=
|
outString +=
|
||||||
text.substring(position, parts.value.index) +
|
text.substring(position, parts.value.index) +
|
||||||
wrapText(
|
wrapText(
|
||||||
@@ -991,7 +1004,15 @@ export class ExcalidrawData {
|
|||||||
generateMD(deletedElements: ExcalidrawElement[] = []): string {
|
generateMD(deletedElements: ExcalidrawElement[] = []): string {
|
||||||
let outString = "# Text Elements\n";
|
let outString = "# Text Elements\n";
|
||||||
for (const key of this.textElements.keys()) {
|
for (const key of this.textElements.keys()) {
|
||||||
outString += `${this.textElements.get(key).raw} ^${key}\n\n`;
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
|
||||||
|
const element = this.scene.elements.filter((el:any)=>el.id===key);
|
||||||
|
let elementString = this.textElements.get(key).raw;
|
||||||
|
if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) {
|
||||||
|
if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
|
||||||
|
elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outString += `${elementString} ^${key}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of this.elementLinks.keys()) {
|
for (const key of this.elementLinks.keys()) {
|
||||||
@@ -1009,7 +1030,13 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
if (this.files.size > 0) {
|
if (this.files.size > 0) {
|
||||||
for (const key of this.files.keys()) {
|
for (const key of this.files.keys()) {
|
||||||
outString += `${key}: [[${this.files.get(key).linkParts.original}]]\n`;
|
const PATHREG = /(^[^#\|]*)/;
|
||||||
|
const ef = this.files.get(key);
|
||||||
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829
|
||||||
|
const path = ef.file
|
||||||
|
? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path))
|
||||||
|
: ef.linkParts.original;
|
||||||
|
outString += `${key}: [[${path}]]\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outString += this.equations.size > 0 || this.files.size > 0 ? "\n" : "";
|
outString += this.equations.size > 0 || this.files.size > 0 ? "\n" : "";
|
||||||
@@ -1060,7 +1087,7 @@ export class ExcalidrawData {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//check if there are any images that need to be processed in the new scene
|
//check if there are any images that need to be processed in the new scene
|
||||||
if (!scene.files || scene.files == {}) {
|
if (!scene.files || Object.keys(scene.files).length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1552,15 +1579,15 @@ export const getTransclusion = async (
|
|||||||
if (!para) {
|
if (!para) {
|
||||||
return { contents: linkParts.original.trim(), lineNum: 0 };
|
return { contents: linkParts.original.trim(), lineNum: 0 };
|
||||||
}
|
}
|
||||||
if (["blockquote", "listItem"].includes(para.type)) {
|
if (["blockquote"].includes(para.type)) {
|
||||||
para = para.children[0];
|
para = para.children[0];
|
||||||
} //blockquotes are special, they have one child, which has the paragraph
|
} //blockquotes are special, they have one child, which has the paragraph
|
||||||
const startPos = para.position.start.offset;
|
const startPos = para.position.start.offset;
|
||||||
const lineNum = para.position.start.line;
|
const lineNum = para.position.start.line;
|
||||||
const endPos =
|
const endPos = para.position.end.offset; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/853
|
||||||
para.children[para.children.length - 1]?.position.start.offset - 1; //alternative: filter((c:any)=>c.type=="blockid")[0]
|
//para.children[para.children.length - 1]?.position.start.offset - 1; //!not clear what the side effect of the #853 change is
|
||||||
return {
|
return {
|
||||||
contents: contents.substring(startPos, endPos).trim(),
|
contents: contents.substring(startPos, endPos).replaceAll(/ \^\S*$|^\^\S*$/gm,"").trim(), //remove the block reference from the end of the line, or from the beginning of a new line
|
||||||
lineNum,
|
lineNum,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-66
@@ -37,6 +37,7 @@ import {
|
|||||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||||
FULLSCREEN_ICON_NAME,
|
FULLSCREEN_ICON_NAME,
|
||||||
IMAGE_TYPES,
|
IMAGE_TYPES,
|
||||||
|
CTRL_OR_CMD,
|
||||||
REG_LINKINDEX_INVALIDCHARS,
|
REG_LINKINDEX_INVALIDCHARS,
|
||||||
KEYCODE,
|
KEYCODE,
|
||||||
LOCAL_PROTOCOL,
|
LOCAL_PROTOCOL,
|
||||||
@@ -70,13 +71,11 @@ import {
|
|||||||
getExportPadding,
|
getExportPadding,
|
||||||
getWithBackground,
|
getWithBackground,
|
||||||
hasExportTheme,
|
hasExportTheme,
|
||||||
|
isVersionNewerThanOther,
|
||||||
scaleLoadedImage,
|
scaleLoadedImage,
|
||||||
|
setDocLeftHandedMode,
|
||||||
svgToBase64,
|
svgToBase64,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
isCtrlDown,
|
|
||||||
isShiftDown,
|
|
||||||
isAltDown,
|
|
||||||
isMetaDown,
|
|
||||||
} from "./utils/Utils";
|
} from "./utils/Utils";
|
||||||
import { getNewOrAdjacentLeaf, getParentOfClass } from "./utils/ObsidianUtils";
|
import { getNewOrAdjacentLeaf, getParentOfClass } from "./utils/ObsidianUtils";
|
||||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||||
@@ -192,17 +191,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
private onKeyUp: (e: KeyboardEvent) => void;
|
private onKeyUp: (e: KeyboardEvent) => void;
|
||||||
private onKeyDown:(e: KeyboardEvent) => void;
|
private onKeyDown:(e: KeyboardEvent) => void;
|
||||||
//store key state for view mode link resolution
|
//store key state for view mode link resolution
|
||||||
private lastKeyboardEvent = new KeyboardEvent("keyboard", {
|
private metaKeyDown: boolean = false;
|
||||||
key: "",
|
private ctrlKeyDown: boolean = false;
|
||||||
code: "",
|
private shiftKeyDown: boolean = false;
|
||||||
location: 0,
|
private altKeyDown: boolean = false;
|
||||||
ctrlKey: false,
|
|
||||||
shiftKey: false,
|
|
||||||
altKey: false,
|
|
||||||
metaKey: false,
|
|
||||||
repeat: false
|
|
||||||
});
|
|
||||||
|
|
||||||
//Obsidian 0.15.0
|
//Obsidian 0.15.0
|
||||||
public ownerWindow: Window;
|
public ownerWindow: Window;
|
||||||
public ownerDocument: Document;
|
public ownerDocument: Document;
|
||||||
@@ -314,7 +306,6 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (!this.getScene || !this.file) {
|
if (!this.getScene || !this.file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
const prompt = new Prompt(
|
const prompt = new Prompt(
|
||||||
app,
|
app,
|
||||||
@@ -659,7 +650,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||||
this.toolsPanelRef.current.setFullscreen(true);
|
this.toolsPanelRef.current.setFullscreen(true);
|
||||||
}
|
}
|
||||||
if (app.isMobile) {
|
if (this.plugin.device.isPhone) {
|
||||||
if(Platform.isIosApp) {
|
if(Platform.isIosApp) {
|
||||||
this.restoreMobileLeaves();
|
this.restoreMobileLeaves();
|
||||||
app.workspace.getLayout().main.children
|
app.workspace.getLayout().main.children
|
||||||
@@ -743,7 +734,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||||
this.toolsPanelRef.current.setFullscreen(false);
|
this.toolsPanelRef.current.setFullscreen(false);
|
||||||
}
|
}
|
||||||
if (app.isMobile) {
|
if (this.plugin.device.isPhone) {
|
||||||
this.restoreMobileLeaves();
|
this.restoreMobileLeaves();
|
||||||
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
||||||
if (oldStylesheet) {
|
if (oldStylesheet) {
|
||||||
@@ -884,7 +875,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
await this.save(false); //in case pasted images haven't been saved yet
|
await this.save(false); //in case pasted images haven't been saved yet
|
||||||
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
|
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
|
||||||
if (isAltDown(ev)) {
|
if (ev.altKey) {
|
||||||
const ef = this.excalidrawData.getFile(selectedImage.fileId);
|
const ef = this.excalidrawData.getFile(selectedImage.fileId);
|
||||||
if (
|
if (
|
||||||
ef.file.extension === "md" &&
|
ef.file.extension === "md" &&
|
||||||
@@ -938,18 +929,18 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isShiftDown(ev) && this.isFullscreen()) {
|
if (ev.shiftKey && this.isFullscreen()) {
|
||||||
this.exitFullscreen();
|
this.exitFullscreen();
|
||||||
}
|
}
|
||||||
if (!file) {
|
if (!file) {
|
||||||
new NewFileActions(this.plugin, linkText, isShiftDown(ev), !app.isMobile && isMetaDown(ev), view).open();
|
new NewFileActions(this.plugin, linkText, ev.shiftKey, !app.isMobile && ev.metaKey, view).open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const leaf =
|
const leaf =
|
||||||
(!app.isMobile && ((isMetaDown(ev) && this.linksAlwaysOpenInANewPane) || isMetaDown(ev)))
|
(!app.isMobile && ((ev.metaKey && this.linksAlwaysOpenInANewPane) || ev.metaKey))
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
? app.workspace.openPopoutLeaf()
|
? app.workspace.openPopoutLeaf()
|
||||||
: (isShiftDown(ev) || this.linksAlwaysOpenInANewPane)
|
: (ev.shiftKey || this.linksAlwaysOpenInANewPane)
|
||||||
? getNewOrAdjacentLeaf(this.plugin, view.leaf)
|
? getNewOrAdjacentLeaf(this.plugin, view.leaf)
|
||||||
: view.leaf;
|
: view.leaf;
|
||||||
await leaf.openFile(file, subpath ? { active: false, eState: { subpath } } : undefined); //if file exists open file and jump to reference
|
await leaf.openFile(file, subpath ? { active: false, eState: { subpath } } : undefined); //if file exists open file and jump to reference
|
||||||
@@ -1071,25 +1062,18 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
await self.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
|
await self.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
|
||||||
self.addParentMoveObserver();
|
self.addParentMoveObserver();
|
||||||
|
|
||||||
const cloneEvent = (e: KeyboardEvent):KeyboardEvent => {
|
|
||||||
return new KeyboardEvent(e.type,{
|
|
||||||
key: e.key,
|
|
||||||
code: e.code,
|
|
||||||
location: e.location,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
shiftKey: e.shiftKey,
|
|
||||||
altKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
repeat: e.repeat
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.onKeyUp = (e: KeyboardEvent) => {
|
self.onKeyUp = (e: KeyboardEvent) => {
|
||||||
self.lastKeyboardEvent = cloneEvent(e);
|
self.ctrlKeyDown = e[CTRL_OR_CMD];
|
||||||
|
self.shiftKeyDown = e.shiftKey;
|
||||||
|
self.altKeyDown = e.altKey;
|
||||||
|
self.metaKeyDown = e.metaKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.onKeyDown = (e: KeyboardEvent) => {
|
self.onKeyDown = (e: KeyboardEvent) => {
|
||||||
self.lastKeyboardEvent = cloneEvent(e);
|
this.ctrlKeyDown = e[CTRL_OR_CMD];
|
||||||
|
this.shiftKeyDown = e.shiftKey;
|
||||||
|
this.altKeyDown = e.altKey;
|
||||||
|
this.metaKeyDown = e.metaKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.ownerWindow.addEventListener("keydown", self.onKeyDown, false);
|
self.ownerWindow.addEventListener("keydown", self.onKeyDown, false);
|
||||||
@@ -1271,7 +1255,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.autosaveTimer = setTimeout(
|
this.autosaveTimer = setTimeout(
|
||||||
timer,
|
timer,
|
||||||
this.plugin.activeExcalidrawView === this &&
|
this.plugin.activeExcalidrawView === this &&
|
||||||
this.semaphores.dirty
|
this.semaphores.dirty &&
|
||||||
|
this.plugin.settings.autosave
|
||||||
? 1000 //try again in 1 second
|
? 1000 //try again in 1 second
|
||||||
: this.plugin.settings.autosaveInterval,
|
: this.plugin.settings.autosaveInterval,
|
||||||
);
|
);
|
||||||
@@ -1281,12 +1266,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
clearTimeout(this.autosaveTimer);
|
clearTimeout(this.autosaveTimer);
|
||||||
this.autosaveTimer = null;
|
this.autosaveTimer = null;
|
||||||
} // clear previous timer if one exists
|
} // clear previous timer if one exists
|
||||||
if (this.plugin.settings.autosave) {
|
this.autosaveTimer = setTimeout(
|
||||||
this.autosaveTimer = setTimeout(
|
timer,
|
||||||
timer,
|
this.plugin.settings.autosaveInterval,
|
||||||
this.plugin.settings.autosaveInterval,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//save current drawing when user closes workspace leaf
|
//save current drawing when user closes workspace leaf
|
||||||
@@ -1456,7 +1439,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.previousSceneVersion = 0;
|
this.previousSceneVersion = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLoaded: boolean = false;
|
public isLoaded: boolean = false;
|
||||||
async setViewData(data: string, clear: boolean = false) {
|
async setViewData(data: string, clear: boolean = false) {
|
||||||
if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion(app);
|
if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion(app);
|
||||||
this.isLoaded = false;
|
this.isLoaded = false;
|
||||||
@@ -1949,7 +1932,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (!this.getScene || !this.file) {
|
if (!this.getScene || !this.file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isCtrlDown(ev)) {
|
if (ev[CTRL_OR_CMD]) {
|
||||||
const png = await this.png(this.getScene());
|
const png = await this.png(this.getScene());
|
||||||
if (!png) {
|
if (!png) {
|
||||||
return;
|
return;
|
||||||
@@ -1976,7 +1959,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (!this.getScene || !this.file) {
|
if (!this.getScene || !this.file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isCtrlDown(ev)) {
|
if (ev[CTRL_OR_CMD]) {
|
||||||
let svg = await this.svg(this.getScene());
|
let svg = await this.svg(this.getScene());
|
||||||
if (!svg) {
|
if (!svg) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2039,7 +2022,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
let selectedTextElement: { id: string; text: string } = null;
|
let selectedTextElement: { id: string; text: string } = null;
|
||||||
let selectedImageElement: { id: string; fileId: string } = null;
|
let selectedImageElement: { id: string; fileId: string } = null;
|
||||||
let selectedElementWithLink: { id: string; text: string } = null;
|
let selectedElementWithLink: { id: string; text: string } = null;
|
||||||
let timestamp = 0;
|
let timestamp = Date.now();
|
||||||
let blockOnMouseButtonDown = false;
|
let blockOnMouseButtonDown = false;
|
||||||
|
|
||||||
this.toolsPanelRef = toolsPanelRef;
|
this.toolsPanelRef = toolsPanelRef;
|
||||||
@@ -2370,7 +2353,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
elements,
|
elements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
},
|
},
|
||||||
false,
|
true, //set to true because svtToExcalidraw generates a legacy Excalidraw object
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2691,7 +2674,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.exitFullscreen();
|
this.exitFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCtrlDown(e) && !isShiftDown(e) && !isAltDown(e)) {
|
if (e[CTRL_OR_CMD] && !e.shiftKey && !e.altKey) {
|
||||||
showHoverPreview();
|
showHoverPreview();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2699,10 +2682,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
//onClick: (e: MouseEvent): any => {
|
//onClick: (e: MouseEvent): any => {
|
||||||
//to onPointerDown so touch events also open links on the iPad (with a keyboard)
|
//to onPointerDown so touch events also open links on the iPad (with a keyboard)
|
||||||
onPointerDown: (e: PointerEvent) => {
|
onPointerDown: (e: PointerEvent) => {
|
||||||
if ( !(isCtrlDown(e) || isMetaDown(e)) ) {
|
if (!(e[CTRL_OR_CMD]||e.metaKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.plugin.settings.allowCtrlClick && !isMetaDown(e)) {
|
if (!this.plugin.settings.allowCtrlClick && !e.metaKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9.
|
//added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9.
|
||||||
@@ -2779,8 +2762,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//dobule click
|
//dobule click
|
||||||
const now = new Date().getTime();
|
const now = Date.now();
|
||||||
if (now - timestamp < 600) {
|
if ((now - timestamp) < 600 && (now - timestamp) > 40) {
|
||||||
handleLinkClick();
|
handleLinkClick();
|
||||||
}
|
}
|
||||||
timestamp = now;
|
timestamp = now;
|
||||||
@@ -2799,14 +2782,24 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
libraryReturnUrl: "app://obsidian.md",
|
libraryReturnUrl: "app://obsidian.md",
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
onChange: (et: ExcalidrawElement[], st: AppState) => {
|
onChange: (et: ExcalidrawElement[], st: AppState) => {
|
||||||
|
const canvasColorChangeHook = () => {
|
||||||
|
if(this.plugin.ea.onCanvasColorChangeHook) {
|
||||||
|
this.plugin.ea.onCanvasColorChangeHook(
|
||||||
|
this.plugin.ea,
|
||||||
|
this,
|
||||||
|
st.viewBackgroundColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModeEnabled = st.viewModeEnabled;
|
viewModeEnabled = st.viewModeEnabled;
|
||||||
if (this.semaphores.justLoaded) {
|
if (this.semaphores.justLoaded) {
|
||||||
this.semaphores.justLoaded = false;
|
this.semaphores.justLoaded = false;
|
||||||
if (!this.semaphores.preventAutozoom) {
|
if (!this.semaphores.preventAutozoom) {
|
||||||
this.zoomToFit(false);
|
this.zoomToFit(false,true);
|
||||||
}
|
}
|
||||||
this.previousSceneVersion = this.getSceneVersion(et);
|
this.previousSceneVersion = this.getSceneVersion(et);
|
||||||
this.previousBackgroundColor = st.viewBackgroundColor;
|
this.previousBackgroundColor = st.viewBackgroundColor;
|
||||||
|
canvasColorChangeHook();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.semaphores.dirty) {
|
if (this.semaphores.dirty) {
|
||||||
@@ -2831,6 +2824,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.previousSceneVersion = sceneVersion;
|
this.previousSceneVersion = sceneVersion;
|
||||||
this.previousBackgroundColor = st.viewBackgroundColor;
|
this.previousBackgroundColor = st.viewBackgroundColor;
|
||||||
this.setDirty(6);
|
this.setDirty(6);
|
||||||
|
canvasColorChangeHook();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2907,7 +2901,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
||||||
//event[CTRL_OR_CMD] = event.shiftKey || event[CTRL_OR_CMD]; Moved logic to isCtrlDown in Utils
|
event[CTRL_OR_CMD] = event.shiftKey || event[CTRL_OR_CMD];
|
||||||
switch (draggable?.type) {
|
switch (draggable?.type) {
|
||||||
case "file":
|
case "file":
|
||||||
if (!onDropHook("file", [draggable.file], null)) {
|
if (!onDropHook("file", [draggable.file], null)) {
|
||||||
@@ -2917,7 +2911,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
isCtrlDown(event) &&
|
event[CTRL_OR_CMD] &&
|
||||||
(IMAGE_TYPES.contains(draggable.file.extension) ||
|
(IMAGE_TYPES.contains(draggable.file.extension) ||
|
||||||
draggable.file.extension === "md")
|
draggable.file.extension === "md")
|
||||||
) {
|
) {
|
||||||
@@ -2930,6 +2924,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
currentPosition.x,
|
currentPosition.x,
|
||||||
currentPosition.y,
|
currentPosition.y,
|
||||||
draggable.file,
|
draggable.file,
|
||||||
|
!event.altKey,
|
||||||
);
|
);
|
||||||
ea.addElementsToView(false, false, true);
|
ea.addElementsToView(false, false, true);
|
||||||
})();
|
})();
|
||||||
@@ -2947,7 +2942,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
case "files":
|
case "files":
|
||||||
if (!onDropHook("file", draggable.files, null)) {
|
if (!onDropHook("file", draggable.files, null)) {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (isCtrlDown(event)) {
|
if (event[CTRL_OR_CMD]) {
|
||||||
const ea = this.plugin.ea;
|
const ea = this.plugin.ea;
|
||||||
ea.reset();
|
ea.reset();
|
||||||
ea.setView(this);
|
ea.setView(this);
|
||||||
@@ -2959,6 +2954,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
currentPosition.x + counter*50,
|
currentPosition.x + counter*50,
|
||||||
currentPosition.y + counter*50,
|
currentPosition.y + counter*50,
|
||||||
f,
|
f,
|
||||||
|
!event.altKey,
|
||||||
);
|
);
|
||||||
counter++;
|
counter++;
|
||||||
await ea.addElementsToView(false, false, true);
|
await ea.addElementsToView(false, false, true);
|
||||||
@@ -3240,10 +3236,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const useNewLeaf =
|
const useNewLeaf =
|
||||||
isShiftDown(event) ||
|
event.shiftKey ||
|
||||||
isCtrlDown(event) ||
|
event[CTRL_OR_CMD] ||
|
||||||
this.linksAlwaysOpenInANewPane ||
|
this.linksAlwaysOpenInANewPane ||
|
||||||
isMetaDown(event);
|
event.metaKey;
|
||||||
|
|
||||||
if (useNewLeaf && this.isFullscreen()) {
|
if (useNewLeaf && this.isFullscreen()) {
|
||||||
this.exitFullscreen();
|
this.exitFullscreen();
|
||||||
@@ -3253,7 +3249,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.plugin,
|
this.plugin,
|
||||||
linkText,
|
linkText,
|
||||||
useNewLeaf,
|
useNewLeaf,
|
||||||
!app.isMobile && isMetaDown(event),
|
!app.isMobile && event.metaKey,
|
||||||
this,
|
this,
|
||||||
).open();
|
).open();
|
||||||
return;
|
return;
|
||||||
@@ -3267,7 +3263,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const leaf = useNewLeaf
|
const leaf = useNewLeaf
|
||||||
? (isMetaDown(event) && !app.isMobile)
|
? (event.metaKey && !app.isMobile)
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
? app.workspace.openPopoutLeaf()
|
? app.workspace.openPopoutLeaf()
|
||||||
: getNewOrAdjacentLeaf(this.plugin, this.leaf)
|
: getNewOrAdjacentLeaf(this.plugin, this.leaf)
|
||||||
@@ -3293,7 +3289,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (
|
if (
|
||||||
element &&
|
element &&
|
||||||
(this.plugin.settings.hoverPreviewWithoutCTRL ||
|
(this.plugin.settings.hoverPreviewWithoutCTRL ||
|
||||||
isCtrlDown(event))
|
event[CTRL_OR_CMD])
|
||||||
) {
|
) {
|
||||||
mouseEvent = event;
|
mouseEvent = event;
|
||||||
mouseEvent.ctrlKey = true;
|
mouseEvent.ctrlKey = true;
|
||||||
@@ -3399,13 +3395,17 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public zoomToFit(delay: boolean = true) {
|
public zoomToFit(delay: boolean = true, justLoaded: boolean = false) {
|
||||||
const api = this.excalidrawAPI;
|
const api = this.excalidrawAPI;
|
||||||
if (!api || !this.excalidrawRef || this.semaphores.isEditingText) {
|
if (!api || !this.excalidrawRef || this.semaphores.isEditingText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
||||||
const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000);
|
const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000);
|
||||||
|
if((app.isMobile && elements.length>1000) || elements.length>2500) {
|
||||||
|
if(justLoaded) api.scrollToContent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (delay) {
|
if (delay) {
|
||||||
//time for the DOM to render, I am sure there is a more elegant solution
|
//time for the DOM to render, I am sure there is a more elegant solution
|
||||||
setTimeout(
|
setTimeout(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const updateEquation = async (
|
|||||||
created: data.created,
|
created: data.created,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
hasSVGwithBitmap: false,
|
hasSVGwithBitmap: false,
|
||||||
|
shouldScale: true,
|
||||||
});
|
});
|
||||||
addFiles(files, view);
|
addFiles(files, view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
TFile,
|
TFile,
|
||||||
Vault,
|
Vault,
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import { RERENDER_EVENT } from "./Constants";
|
import { CTRL_OR_CMD, RERENDER_EVENT } from "./Constants";
|
||||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||||
import { createPNG, createSVG } from "./ExcalidrawAutomate";
|
import { createPNG, createSVG } from "./ExcalidrawAutomate";
|
||||||
import { ExportSettings } from "./ExcalidrawView";
|
import { ExportSettings } from "./ExcalidrawView";
|
||||||
@@ -19,8 +19,6 @@ import {
|
|||||||
getWithBackground,
|
getWithBackground,
|
||||||
hasExportTheme,
|
hasExportTheme,
|
||||||
svgToBase64,
|
svgToBase64,
|
||||||
isCtrlDown,
|
|
||||||
isMetaDown,
|
|
||||||
} from "./utils/Utils";
|
} from "./utils/Utils";
|
||||||
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
|
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||||
|
|
||||||
@@ -213,15 +211,15 @@ const createImageDiv = async (
|
|||||||
if(!srcParts) return;
|
if(!srcParts) return;
|
||||||
plugin.openDrawing(
|
plugin.openDrawing(
|
||||||
vault.getAbstractFileByPath(srcParts[1]) as TFile,
|
vault.getAbstractFileByPath(srcParts[1]) as TFile,
|
||||||
isCtrlDown(ev)
|
ev[CTRL_OR_CMD]
|
||||||
? "new-pane"
|
? "new-pane"
|
||||||
: (isMetaDown(ev) && !app.isMobile)
|
: (ev.metaKey && !app.isMobile)
|
||||||
? "popout-window"
|
? "popout-window"
|
||||||
: "active-pane",
|
: "active-pane",
|
||||||
true,
|
true,
|
||||||
srcParts[2],
|
srcParts[2],
|
||||||
);
|
);
|
||||||
}
|
} //.ctrlKey||ev.metaKey);
|
||||||
});
|
});
|
||||||
el.addEventListener(RERENDER_EVENT, async (e) => {
|
el.addEventListener(RERENDER_EVENT, async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -362,7 +360,13 @@ const tmpObsidianWYSIWYG = async (
|
|||||||
internalEmbedDiv = internalEmbedDiv.parentElement;
|
internalEmbedDiv = internalEmbedDiv.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!internalEmbedDiv.hasClass("internal-embed")) return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
|
if(
|
||||||
|
internalEmbedDiv.hasClass("dataview") ||
|
||||||
|
internalEmbedDiv.hasClass("cm-preview-code-block") ||
|
||||||
|
internalEmbedDiv.hasClass("cm-embed-block")
|
||||||
|
) {
|
||||||
|
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
|
||||||
|
}
|
||||||
|
|
||||||
const attr: imgElementAttributes = {
|
const attr: imgElementAttributes = {
|
||||||
fname: ctx.sourcePath,
|
fname: ctx.sourcePath,
|
||||||
@@ -546,13 +550,13 @@ export const observer = new MutationObserver(async (m) => {
|
|||||||
if (src) {
|
if (src) {
|
||||||
plugin.openDrawing(
|
plugin.openDrawing(
|
||||||
vault.getAbstractFileByPath(src) as TFile,
|
vault.getAbstractFileByPath(src) as TFile,
|
||||||
isCtrlDown(ev)
|
ev[CTRL_OR_CMD]
|
||||||
? "new-pane"
|
? "new-pane"
|
||||||
: (isMetaDown(ev) && !app.isMobile)
|
: (ev.metaKey && !app.isMobile)
|
||||||
? "popout-window"
|
? "popout-window"
|
||||||
: "active-pane",
|
: "active-pane",
|
||||||
);
|
);
|
||||||
}
|
} //.ctrlKey||ev.metaKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
node.appendChild(div);
|
node.appendChild(div);
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*#]/g;
|
|||||||
export const REG_BLOCK_REF_CLEAN =
|
export const REG_BLOCK_REF_CLEAN =
|
||||||
/[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g; //https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
|
/[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g; //https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
|
||||||
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
|
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
|
||||||
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg"];
|
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico"];
|
||||||
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
|
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
|
||||||
export const MAX_IMAGE_SIZE = 500;
|
export const MAX_IMAGE_SIZE = 500;
|
||||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||||
|
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
|
||||||
|
import ExcalidrawView from "../ExcalidrawView";
|
||||||
|
import { t } from "../lang/helpers";
|
||||||
|
import ExcalidrawPlugin from "../main";
|
||||||
|
|
||||||
|
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||||
|
public app: App;
|
||||||
|
public plugin: ExcalidrawPlugin;
|
||||||
|
private view: ExcalidrawView;
|
||||||
|
|
||||||
|
constructor(plugin: ExcalidrawPlugin) {
|
||||||
|
super(plugin.app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.app = plugin.app;
|
||||||
|
this.limit = 20;
|
||||||
|
this.setInstructions([
|
||||||
|
{
|
||||||
|
command: t("SELECT_FILE"),
|
||||||
|
purpose: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.setPlaceholder(t("SELECT_DRAWING"));
|
||||||
|
this.emptyStateText = t("NO_MATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
getItems(): TFile[] {
|
||||||
|
return (this.app.vault.getFiles() || []).filter(
|
||||||
|
(f: TFile) => f.extension === "svg" &&
|
||||||
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
|
||||||
|
!f.path.match(REG_LINKINDEX_INVALIDCHARS),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemText(item: TFile): string {
|
||||||
|
return item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
|
||||||
|
if(!item) return;
|
||||||
|
const ea = this.plugin.ea;
|
||||||
|
ea.reset();
|
||||||
|
ea.setView(this.view);
|
||||||
|
const svg = await app.vault.read(item);
|
||||||
|
if(!svg || svg === "") return;
|
||||||
|
ea.importSVG(svg);
|
||||||
|
ea.addElementsToView(true, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(view: ExcalidrawView) {
|
||||||
|
this.view = view;
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,23 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
|||||||
this.limit = 20;
|
this.limit = 20;
|
||||||
this.setInstructions([
|
this.setInstructions([
|
||||||
{
|
{
|
||||||
command: t("SELECT_FILE"),
|
command: t("SELECT_FILE_WITH_OPTION_TO_SCALE"),
|
||||||
purpose: "",
|
purpose: "",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
this.setPlaceholder(t("SELECT_DRAWING"));
|
this.setPlaceholder(t("SELECT_DRAWING"));
|
||||||
this.emptyStateText = t("NO_MATCH");
|
this.emptyStateText = t("NO_MATCH");
|
||||||
|
this.inputEl.onkeyup = (e) => {
|
||||||
|
//@ts-ignore
|
||||||
|
if (e.key === "Enter" && e.altKey && this.chooser.values) {
|
||||||
|
this.onChooseItem(
|
||||||
|
//@ts-ignore
|
||||||
|
this.chooser.values[this.chooser.selectedItem].item,
|
||||||
|
new KeyboardEvent("keypress",{altKey: true})
|
||||||
|
);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getItems(): TFile[] {
|
getItems(): TFile[] {
|
||||||
@@ -39,13 +50,13 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
|||||||
return item.path;
|
return item.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChooseItem(item: TFile): void {
|
onChooseItem(item: TFile, event: KeyboardEvent): void {
|
||||||
const ea = this.plugin.ea;
|
const ea = this.plugin.ea;
|
||||||
ea.reset();
|
ea.reset();
|
||||||
ea.setView(this.view);
|
ea.setView(this.view);
|
||||||
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
|
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
|
||||||
(async () => {
|
(async () => {
|
||||||
await ea.addImage(0, 0, item);
|
await ea.addImage(0, 0, item, !event.altKey);
|
||||||
ea.addElementsToView(true, false, true);
|
ea.addElementsToView(true, false, true);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,60 @@ I develop this plugin as a hobby, spending most of my free time doing this. If y
|
|||||||
|
|
||||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||||
`,
|
`,
|
||||||
|
"1.7.26":`## Fixed
|
||||||
|
- Transcluded block with a parent bullet does not embed sub-bullet [#853](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/853)
|
||||||
|
- Transcluded text will now exclude ^block-references at end of lines
|
||||||
|
- Phantom duplicates of the drawing appear when "zoom to fit" results in a zoom value below 10% and there are many objects on the canvas [#850](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/850)
|
||||||
|
- CTRL+Wheel will increase/decrease zoom in steps of 5% matching the behavior of the "+" & "-" zoom buttons.
|
||||||
|
- Latest updates from Excalidarw.com
|
||||||
|
- Freedraw flip not scaling correctly [#5752](https://github.com/excalidraw/excalidraw/pull/5752)
|
||||||
|
- Multiple elements resizing regressions [#5586](https://github.com/excalidraw/excalidraw/pull/5586)
|
||||||
|
|
||||||
|
## New - power user features
|
||||||
|
- Force the embedded image to always scale to 100%. Note: this is a very niche feature with a very particular behavior that I built primarily for myself (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉)... This will reset your embedded image to 100% size every time you open the Excalidraw drawing, or in case you have embedded an Excalidraw drawing on your canvas inserted using this function, every time you update the embedded drawing, it will be scaled back to 100% size. This means that even if you resize the image on the drawing, it will reset to 100% the next time you open the file or you modify the original embedded object. This feature is useful when you decompose a drawing into separate Excalidraw files, but when combined onto a single canvas you want the individual pieces to maintain their actual sizes. I use this feature to construct Book-on-a-Page summaries from atomic drawings.
|
||||||
|
- I added an action to the command palette to temporarily disable/enable Excalidraw autosave. When autosave is disabled, Excalidraw will still save your drawing when changing to another Obsidian window, but it will not save every 10 seconds. On a mobile device (but also on a desktop) this can lead to data loss if you terminate Obsidian abruptly (i.e. swipe the application away, or close Obsidian without first closing the drawing). Use this feature if you find Excalidraw laggy.`,
|
||||||
|
"1.7.25":`## Fixed
|
||||||
|
- Tool buttons did not "stick" the first time you clicked them.
|
||||||
|
- Tray (in tray mode) was higher when the help button was visible. The tray in tablet mode was too large and the help button was missing.
|
||||||
|
- ExcalidrawAutomate ${String.fromCharCode(96)}getCM(color:TInput): ColorMaster;${String.fromCharCode(96)} function will now properly convert valid [css color names](https://www.w3schools.com/colors/colors_names.asp) to ColorMaster objects.
|
||||||
|
- The downloaded script icons in the Excalidraw-Obsidian menu were not always correct
|
||||||
|
- The obsidian mobile navigation bar at the bottom overlapped with Excalidraw
|
||||||
|
|
||||||
|
## New
|
||||||
|
- Created ExcalidrawAutomate hook for styling script when the canvas color changes. See sample [onCanvasColorChangeHook](https://gist.github.com/zsviczian/c7223c5b4af30d5c88a0cae05300305c) implementation following the link.
|
||||||
|
|
||||||
|
<div class="excalidraw-videoWrapper"><div>
|
||||||
|
<iframe src="https://www.youtube.com/embed/LtR04fNTKTM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
|
${String.fromCharCode(96, 96, 96)}typescript
|
||||||
|
/**
|
||||||
|
* If set, this callback is triggered whenever the active canvas color changes
|
||||||
|
*/
|
||||||
|
onCanvasColorChangeHook: (
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
view: ExcalidrawView, //the Excalidraw view
|
||||||
|
color: string,
|
||||||
|
) => void = null;
|
||||||
|
${String.fromCharCode(96, 96, 96)}
|
||||||
|
`,
|
||||||
|
"1.7.24":`
|
||||||
|
# New and improved
|
||||||
|
- **Updated Chinese translation**. Thanks, @tswwe!
|
||||||
|
- **Improved update for TextElement links**: Until now, when you attached a link to a file to a TextElement using the "Create Link" command, this link did not get updated when the file was renamed or moved. Only links created as markdown links in the TextElement text were updated. Now both approaches work. Keep in mind however, that if you have a link in the TextElemenet text, it will override the link attached to the text element using the create link command. [#566](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566)
|
||||||
|
- **Transclusion filters markdown comments**: Text transclusion in a TextElement using the ${String.fromCharCode(96)}![[file]]${String.fromCharCode(96)} or ${String.fromCharCode(96)}![[file#section]]${String.fromCharCode(96)} format did not filter out markdown comments in the file placed ${String.fromCharCode(96)}%% inside a comment block %%${String.fromCharCode(96)}. Now they do.
|
||||||
|
- **Remove leading '>' from trancluded quotes**: Added a new option in settings under **Links and Transclusion** to remove the leading ${String.fromCharCode(96)}> ${String.fromCharCode(96)} characters from quotes you transclude as a text element in your drawing.
|
||||||
|

|
||||||
|
- **Added support for ${String.fromCharCode(96)}webp${String.fromCharCode(96)}, ${String.fromCharCode(96)}bmp${String.fromCharCode(96)}, and ${String.fromCharCode(96)}ico${String.fromCharCode(96)} images**. This extends the already supported formats (${String.fromCharCode(96)}jpg${String.fromCharCode(96)}, ${String.fromCharCode(96)}gif${String.fromCharCode(96)}, ${String.fromCharCode(96)}png${String.fromCharCode(96)}, ${String.fromCharCode(96)}svg${String.fromCharCode(96)}).
|
||||||
|
- **Added command palette action to reset images to original size**. Select a single image or embedded Excalidraw drawing on your canvas and choose ${String.fromCharCode(96)}Set selected image element size to 100% of original${String.fromCharCode(96)} from the command palette. This function is especially helpful when you combine atomic drawings on a single canvas, keeping each atomic piece in its original excalidraw file (i.e. the way I create [book on a page summaries](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1-mbCYc3T7mr-unmsIXpEG))
|
||||||
|
- The ${String.fromCharCode(96)}async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>${String.fromCharCode(96)} function is also avaiable via ExcalidrawAutomate. You may use this function to resize images to custom scales (e.g. 50% size, or to fit a certain bounding rectangle).
|
||||||
|
|
||||||
|
# Fixed
|
||||||
|
- **Upgraded perfect freehand package to resolve unwanted dots on end of lines** [#5727](https://github.com/excalidraw/excalidraw/pull/5727)
|
||||||
|
- **Pinch zoom in View mode opens images** resulting in a very annoying behavior [#837](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/837)
|
||||||
|
- **Embedded files** such as transcluded markdown documents and images **did not honor the Obsidian "New Link Format" setting** (shortest path, relative path, absolute path). [#829](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829)
|
||||||
|
- **Fixed error with dataview queries involving Excalidraw files**: In case you created a task on an Excalidraw canvas (${String.fromCharCode(96)}docA.md${String.fromCharCode(96)}) by typing ${String.fromCharCode(96)}- [ ] Task [[owner]] #tag${String.fromCharCode(96)}, and then you created a Dataview tasklist in another document (${String.fromCharCode(96)}docB.md${String.fromCharCode(96)}) such that the query criteria matched the task in ${String.fromCharCode(96)}docA.md${String.fromCharCode(96)}, then the task from ${String.fromCharCode(96)}docA.md${String.fromCharCode(96)} only appeared as an empty line when viewing ${String.fromCharCode(96)}docB.md${String.fromCharCode(96)}. If you now embedded ${String.fromCharCode(96)}docB.md${String.fromCharCode(96)} into a third markdown document (${String.fromCharCode(96)}docC.md${String.fromCharCode(96)}), then instead of the contents of ${String.fromCharCode(96)}docB.md${String.fromCharCode(96)} Obsidian rendered ${String.fromCharCode(96)}docA.md${String.fromCharCode(96)}. [#835](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835)
|
||||||
|
`,
|
||||||
"1.7.22":`
|
"1.7.22":`
|
||||||
# Fixed
|
# Fixed
|
||||||
- Text size in sticky notes increased when opening the drawing and when editing a sticky note [#824](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/824)
|
- Text size in sticky notes increased when opening the drawing and when editing a sticky note [#824](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/824)
|
||||||
|
|||||||
+11
-3
@@ -52,7 +52,8 @@ export default {
|
|||||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||||
INSERT_LINK: "Insert link to file",
|
INSERT_LINK: "Insert link to file",
|
||||||
INSERT_IMAGE: "Insert image from vault",
|
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
||||||
|
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
|
||||||
INSERT_MD: "Insert markdown file from vault",
|
INSERT_MD: "Insert markdown file from vault",
|
||||||
INSERT_LATEX:
|
INSERT_LATEX:
|
||||||
"Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
|
"Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
|
||||||
@@ -60,6 +61,9 @@ export default {
|
|||||||
READ_RELEASE_NOTES: "Read latest release notes",
|
READ_RELEASE_NOTES: "Read latest release notes",
|
||||||
TRAY_MODE: "Toggle property-panel tray-mode",
|
TRAY_MODE: "Toggle property-panel tray-mode",
|
||||||
SEARCH: "Search for text in drawing",
|
SEARCH: "Search for text in drawing",
|
||||||
|
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
|
||||||
|
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
|
||||||
|
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
|
||||||
|
|
||||||
//ExcalidrawView.ts
|
//ExcalidrawView.ts
|
||||||
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
|
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
|
||||||
@@ -112,7 +116,7 @@ export default {
|
|||||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may omit the .md file extension). " +
|
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may omit the .md file extension). " +
|
||||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy Excalidraw file as well " +
|
"If you are using Excalidraw in compatibility mode, then your template must be a legacy Excalidraw file as well " +
|
||||||
"such as Excalidraw/Template.excalidraw.",
|
"such as Excalidraw/Template.excalidraw.",
|
||||||
SCRIPT_FOLDER_NAME: "Excalidraw Automate script folder",
|
SCRIPT_FOLDER_NAME: "Excalidraw Automate script folder (CASE SeNSitiVE!)",
|
||||||
SCRIPT_FOLDER_DESC:
|
SCRIPT_FOLDER_DESC:
|
||||||
"The files you place in this folder will be treated as Excalidraw Automate scripts. " +
|
"The files you place in this folder will be treated as Excalidraw Automate scripts. " +
|
||||||
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
|
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
|
||||||
@@ -259,6 +263,9 @@ export default {
|
|||||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC:
|
PAGE_TRANSCLUSION_CHARCOUNT_DESC:
|
||||||
"The maximum number of characters to display from the page when transcluding an entire page with the " +
|
"The maximum number of characters to display from the page when transcluding an entire page with the " +
|
||||||
"![[markdown page]] format.",
|
"![[markdown page]] format.",
|
||||||
|
QUOTE_TRANSCLUSION_REMOVE_NAME: "Quote translusion: remove leading '> ' from each line",
|
||||||
|
QUOTE_TRANSCLUSION_REMOVE_DESC: "Remove the leading '> ' from each line of the transclusion. This will improve readibility of quotes in text only transclusions<br>" +
|
||||||
|
"<b>Toggle ON:</b> Remove leading '> '<br><b>Toggle OFF:</b> Do not remove leading '> ' (note it will still be removed from the first row due to Obsidian API functionality)",
|
||||||
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
|
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
|
||||||
GET_URL_TITLE_DESC:
|
GET_URL_TITLE_DESC:
|
||||||
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
|
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
|
||||||
@@ -414,9 +421,10 @@ export default {
|
|||||||
|
|
||||||
//openDrawings.ts
|
//openDrawings.ts
|
||||||
SELECT_FILE: "Select a file then press enter.",
|
SELECT_FILE: "Select a file then press enter.",
|
||||||
|
SELECT_FILE_WITH_OPTION_TO_SCALE: "Select a file then press ENTER, or ALT+ENTER to insert at 100% scale.",
|
||||||
NO_MATCH: "No file matches your query.",
|
NO_MATCH: "No file matches your query.",
|
||||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||||
SELECT_DRAWING: "Select the drawing you want to insert",
|
SELECT_DRAWING: "Select the image or drawing you want to insert",
|
||||||
TYPE_FILENAME: "Type name of drawing to select.",
|
TYPE_FILENAME: "Type name of drawing to select.",
|
||||||
SELECT_FILE_OR_TYPE_NEW:
|
SELECT_FILE_OR_TYPE_NEW:
|
||||||
"Select existing drawing or type name of a new drawing then press Enter.",
|
"Select existing drawing or type name of a new drawing then press Enter.",
|
||||||
|
|||||||
+153
-16
@@ -37,6 +37,7 @@ import {
|
|||||||
JSON_parse,
|
JSON_parse,
|
||||||
nanoid,
|
nanoid,
|
||||||
DARK_BLANK_DRAWING,
|
DARK_BLANK_DRAWING,
|
||||||
|
CTRL_OR_CMD,
|
||||||
SCRIPT_INSTALL_CODEBLOCK,
|
SCRIPT_INSTALL_CODEBLOCK,
|
||||||
SCRIPT_INSTALL_FOLDER,
|
SCRIPT_INSTALL_FOLDER,
|
||||||
VIRGIL_FONT,
|
VIRGIL_FONT,
|
||||||
@@ -57,6 +58,7 @@ import {
|
|||||||
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
||||||
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
||||||
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
||||||
|
import { ImportSVGDialog } from "./dialogs/ImportSVGDialog";
|
||||||
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
||||||
import {
|
import {
|
||||||
initExcalidrawAutomate,
|
initExcalidrawAutomate,
|
||||||
@@ -85,11 +87,10 @@ import {
|
|||||||
debug,
|
debug,
|
||||||
isVersionNewerThanOther,
|
isVersionNewerThanOther,
|
||||||
getExportTheme,
|
getExportTheme,
|
||||||
isCtrlDown,
|
|
||||||
} from "./utils/Utils";
|
} from "./utils/Utils";
|
||||||
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||||
//import { OneOffs } from "./OneOffs";
|
//import { OneOffs } from "./OneOffs";
|
||||||
import { FileId } from "@zsviczian/excalidraw/types/element/types";
|
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
import { ScriptEngine } from "./Scripts";
|
import { ScriptEngine } from "./Scripts";
|
||||||
import {
|
import {
|
||||||
hoverEvent,
|
hoverEvent,
|
||||||
@@ -103,6 +104,7 @@ import { decompressFromBase64 } from "lz-string";
|
|||||||
import { Packages } from "./types";
|
import { Packages } from "./types";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
|
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
|
||||||
|
import { check } from "prettier";
|
||||||
|
|
||||||
|
|
||||||
declare module "obsidian" {
|
declare module "obsidian" {
|
||||||
@@ -138,6 +140,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
private openDialog: OpenFileDialog;
|
private openDialog: OpenFileDialog;
|
||||||
public insertLinkDialog: InsertLinkDialog;
|
public insertLinkDialog: InsertLinkDialog;
|
||||||
public insertImageDialog: InsertImageDialog;
|
public insertImageDialog: InsertImageDialog;
|
||||||
|
public importSVGDialog: ImportSVGDialog;
|
||||||
public insertMDDialog: InsertMDDialog;
|
public insertMDDialog: InsertMDDialog;
|
||||||
public activeExcalidrawView: ExcalidrawView = null;
|
public activeExcalidrawView: ExcalidrawView = null;
|
||||||
public lastActiveExcalidrawFilePath: string = null;
|
public lastActiveExcalidrawFilePath: string = null;
|
||||||
@@ -165,6 +168,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
||||||
public leafChangeTimeout: NodeJS.Timeout = null;
|
public leafChangeTimeout: NodeJS.Timeout = null;
|
||||||
private forceSaveCommand:Command;
|
private forceSaveCommand:Command;
|
||||||
|
public device: {
|
||||||
|
isDesktop: boolean,
|
||||||
|
isPhone: boolean,
|
||||||
|
isTablet: boolean,
|
||||||
|
isMobile: boolean,
|
||||||
|
isLinux: boolean,
|
||||||
|
isMacOS: boolean,
|
||||||
|
isWindows: boolean,
|
||||||
|
isIOS: boolean,
|
||||||
|
isAndroid: boolean
|
||||||
|
};
|
||||||
|
|
||||||
constructor(app: App, manifest: PluginManifest) {
|
constructor(app: App, manifest: PluginManifest) {
|
||||||
super(app, manifest);
|
super(app, manifest);
|
||||||
@@ -175,8 +189,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
this.equationsMaster = new Map<FileId, string>();
|
this.equationsMaster = new Map<FileId, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public getPackage(win:Window):Packages {
|
public getPackage(win:Window):Packages {
|
||||||
if(win===window) {
|
if(win===window) {
|
||||||
return {react, reactDOM, excalidrawLib};
|
return {react, reactDOM, excalidrawLib};
|
||||||
@@ -196,13 +208,26 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
|
this.device = {
|
||||||
|
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
|
||||||
|
isPhone: document.body.hasClass("is-phone"),
|
||||||
|
isTablet: document.body.hasClass("is-tablet"),
|
||||||
|
isMobile: document.body.hasClass("is-mobile"), //running Obsidian Mobile, need to also check isTablet
|
||||||
|
isLinux: document.body.hasClass("mod-linux") && ! document.body.hasClass("is-android"),
|
||||||
|
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
|
||||||
|
isWindows: document.body.hasClass("mod-windows"),
|
||||||
|
isIOS: document.body.hasClass("is-ios"),
|
||||||
|
isAndroid: document.body.hasClass("is-android")
|
||||||
|
}
|
||||||
|
|
||||||
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
||||||
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
||||||
addIcon(DISK_ICON_NAME, DISK_ICON);
|
addIcon(DISK_ICON_NAME, DISK_ICON);
|
||||||
addIcon(PNG_ICON_NAME, PNG_ICON);
|
addIcon(PNG_ICON_NAME, PNG_ICON);
|
||||||
addIcon(SVG_ICON_NAME, SVG_ICON);
|
addIcon(SVG_ICON_NAME, SVG_ICON);
|
||||||
|
|
||||||
await this.loadSettings();
|
await this.loadSettings({reEnableAutosave:true});
|
||||||
|
|
||||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||||
this.ea = await initExcalidrawAutomate(this);
|
this.ea = await initExcalidrawAutomate(this);
|
||||||
|
|
||||||
@@ -689,12 +714,13 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
this.openDialog = new OpenFileDialog(this.app, this);
|
this.openDialog = new OpenFileDialog(this.app, this);
|
||||||
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
||||||
this.insertImageDialog = new InsertImageDialog(this);
|
this.insertImageDialog = new InsertImageDialog(this);
|
||||||
|
this.importSVGDialog = new ImportSVGDialog(this);
|
||||||
this.insertMDDialog = new InsertMDDialog(this);
|
this.insertMDDialog = new InsertMDDialog(this);
|
||||||
|
|
||||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
||||||
this.createAndOpenDrawing(
|
this.createAndOpenDrawing(
|
||||||
getDrawingFilename(this.settings),
|
getDrawingFilename(this.settings),
|
||||||
isCtrlDown(e) ? "new-pane" : "active-pane",
|
e[CTRL_OR_CMD]?"new-pane":"active-pane",
|
||||||
); //.ctrlKey||e.metaKey);
|
); //.ctrlKey||e.metaKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -757,6 +783,28 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "excalidraw-disable-autosave",
|
||||||
|
name: t("TEMPORARY_DISABLE_AUTOSAVE"),
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
if(!this.settings.autosave) return false; //already disabled
|
||||||
|
if(checking) return true;
|
||||||
|
this.settings.autosave = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "excalidraw-enable-autosave",
|
||||||
|
name: t("TEMPORARY_ENABLE_AUTOSAVE"),
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
if(this.settings.autosave) return false; //already enabled
|
||||||
|
if(checking) return true;
|
||||||
|
this.settings.autosave = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "excalidraw-download-lib",
|
id: "excalidraw-download-lib",
|
||||||
name: t("DOWNLOAD_LIBRARY"),
|
name: t("DOWNLOAD_LIBRARY"),
|
||||||
@@ -1141,7 +1189,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||||
(async()=>{
|
(async()=>{
|
||||||
const isLeftHanded = this.settings.isLeftHanded;
|
const isLeftHanded = this.settings.isLeftHanded;
|
||||||
await this.loadSettings(false);
|
await this.loadSettings({applyLefthandedMode: false});
|
||||||
this.settings.isLeftHanded = !isLeftHanded;
|
this.settings.isLeftHanded = !isLeftHanded;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
//not clear why I need to do this. If I don't double apply the stylesheet changes
|
//not clear why I need to do this. If I don't double apply the stylesheet changes
|
||||||
@@ -1153,6 +1201,42 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "reset-image-to-100",
|
||||||
|
name: t("RESET_IMG_TO_100"),
|
||||||
|
checkCallback: (checking:boolean) => {
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||||
|
if(!view) return false;
|
||||||
|
if(!view.excalidrawAPI) return false;
|
||||||
|
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
|
||||||
|
if(els.length !== 1) {
|
||||||
|
if(checking) return false;
|
||||||
|
new Notice("Select a single image element and try again");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const el = els[0] as ExcalidrawImageElement;
|
||||||
|
const ef = view.excalidrawData.getFile(el.fileId);
|
||||||
|
if(!ef) {
|
||||||
|
if(checking) return false;
|
||||||
|
new Notice("Select a single image element and try again");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(checking) return true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const ea = new ExcalidrawAutomate(this,view);
|
||||||
|
const size = await ea.getOriginalImageSize(el);
|
||||||
|
if(size) {
|
||||||
|
ea.copyViewElementsToEAforEditing(els);
|
||||||
|
const eaEl = ea.getElement(el.id);
|
||||||
|
//@ts-ignore
|
||||||
|
eaEl.width = size.width; eaEl.height = size.height;
|
||||||
|
ea.addElementsToView(false,false,false);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "insert-image",
|
id: "insert-image",
|
||||||
name: t("INSERT_IMAGE"),
|
name: t("INSERT_IMAGE"),
|
||||||
@@ -1169,6 +1253,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "import-svg",
|
||||||
|
name: t("IMPORT_SVG"),
|
||||||
|
checkCallback: (checking: boolean) => {
|
||||||
|
if (checking) {
|
||||||
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||||
|
}
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||||
|
if (view) {
|
||||||
|
this.importSVGDialog.start(view);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "release-notes",
|
id: "release-notes",
|
||||||
name: t("READ_RELEASE_NOTES"),
|
name: t("READ_RELEASE_NOTES"),
|
||||||
@@ -1598,7 +1698,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const activeLeafChangeEventHandler = async (leaf: WorkspaceLeaf) => {
|
const activeLeafChangeEventHandler = async (leaf: WorkspaceLeaf) => {
|
||||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
||||||
if(self.leafChangeTimeout) {
|
if(self.leafChangeTimeout) {
|
||||||
clearTimeout(this.leafChangeTimeout);
|
clearTimeout(self.leafChangeTimeout);
|
||||||
}
|
}
|
||||||
self.leafChangeTimeout = setTimeout(()=>{self.leafChangeTimeout = null;},1000);
|
self.leafChangeTimeout = setTimeout(()=>{self.leafChangeTimeout = null;},1000);
|
||||||
|
|
||||||
@@ -1611,6 +1711,25 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
self.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
|
self.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//!Temporary hack
|
||||||
|
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
|
||||||
|
if (app.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
||||||
|
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||||
|
if(navbar && navbar instanceof HTMLDivElement) {
|
||||||
|
navbar.style.position="relative";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
||||||
|
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||||
|
if(navbar && navbar instanceof HTMLDivElement) {
|
||||||
|
navbar.style.position="";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------
|
||||||
|
//----------------------
|
||||||
|
|
||||||
if (previouslyActiveEV && previouslyActiveEV !== newActiveviewEV) {
|
if (previouslyActiveEV && previouslyActiveEV !== newActiveviewEV) {
|
||||||
if (previouslyActiveEV.leaf !== leaf) {
|
if (previouslyActiveEV.leaf !== leaf) {
|
||||||
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
|
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
|
||||||
@@ -1648,20 +1767,32 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
} //refresh embedded files
|
} //refresh embedded files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ( //@ts-ignore
|
||||||
|
newActiveviewEV && newActiveviewEV._loaded &&
|
||||||
|
newActiveviewEV.isLoaded && newActiveviewEV.excalidrawAPI &&
|
||||||
|
self.ea.onCanvasColorChangeHook
|
||||||
|
) {
|
||||||
|
self.ea.onCanvasColorChangeHook(
|
||||||
|
self.ea,
|
||||||
|
newActiveviewEV,
|
||||||
|
newActiveviewEV.excalidrawAPI.getAppState().viewBackgroundColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/300
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/300
|
||||||
if (self.popScope) {
|
if (self.popScope) {
|
||||||
self.popScope();
|
self.popScope();
|
||||||
self.popScope = null;
|
self.popScope = null;
|
||||||
}
|
}
|
||||||
if (newActiveviewEV) {
|
if (newActiveviewEV) {
|
||||||
const scope = this.app.keymap.getRootScope();
|
const scope = self.app.keymap.getRootScope();
|
||||||
const handler = scope.register(["Mod"], "Enter", () => true);
|
const handler = scope.register(["Mod"], "Enter", () => true);
|
||||||
const overridSaveShortcut = (
|
const overridSaveShortcut = (
|
||||||
this.forceSaveCommand &&
|
self.forceSaveCommand &&
|
||||||
this.forceSaveCommand.hotkeys[0].key === "s" &&
|
self.forceSaveCommand.hotkeys[0].key === "s" &&
|
||||||
this.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
|
self.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
|
||||||
)
|
)
|
||||||
const self = this;
|
|
||||||
const saveHandler = overridSaveShortcut
|
const saveHandler = overridSaveShortcut
|
||||||
? scope.register(["Ctrl"], "s", () => self.forceSaveActiveView(false))
|
? scope.register(["Ctrl"], "s", () => self.forceSaveActiveView(false))
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -1912,10 +2043,16 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadSettings(applyLefthandedMode:boolean = true) {
|
public async loadSettings(opts: {
|
||||||
|
applyLefthandedMode?: boolean,
|
||||||
|
reEnableAutosave?: boolean
|
||||||
|
} = {applyLefthandedMode: true, reEnableAutosave: false}
|
||||||
|
) {
|
||||||
|
if(typeof opts.applyLefthandedMode === "undefined") opts.applyLefthandedMode = true;
|
||||||
|
if(typeof opts.reEnableAutosave === "undefined") opts.reEnableAutosave = false;
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
if(applyLefthandedMode) setLeftHandedMode(this.settings.isLeftHanded);
|
if(opts.applyLefthandedMode) setLeftHandedMode(this.settings.isLeftHanded);
|
||||||
this.settings.autosave = true;
|
if(opts.reEnableAutosave) this.settings.autosave = true;
|
||||||
this.settings.autosaveInterval = app.isMobile?10000:15000; //more frequent on mobile because Obsidian may be killed on context switching
|
this.settings.autosaveInterval = app.isMobile?10000:15000; //more frequent on mobile because Obsidian may be killed on context switching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+16
-19
@@ -2,15 +2,14 @@ import clsx from "clsx";
|
|||||||
import { Notice, TFile } from "obsidian";
|
import { Notice, TFile } from "obsidian";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ActionButton } from "./ActionButton";
|
import { ActionButton } from "./ActionButton";
|
||||||
import { ICONS } from "./ActionIcons";
|
import { ICONS, stringToSVG } from "./ActionIcons";
|
||||||
import { SCRIPT_INSTALL_FOLDER } from "../Constants";
|
import { SCRIPT_INSTALL_FOLDER, CTRL_OR_CMD } from "../Constants";
|
||||||
import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
|
import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
|
||||||
import ExcalidrawView, { TextMode } from "../ExcalidrawView";
|
import ExcalidrawView, { TextMode } from "../ExcalidrawView";
|
||||||
import { t } from "../lang/helpers";
|
import { t } from "../lang/helpers";
|
||||||
import { ReleaseNotes } from "../dialogs/ReleaseNotes";
|
import { ReleaseNotes } from "../dialogs/ReleaseNotes";
|
||||||
import { ScriptIconMap } from "../Scripts";
|
import { ScriptIconMap } from "../Scripts";
|
||||||
import { getIMGFilename } from "../utils/FileUtils";
|
import { getIMGFilename } from "../utils/FileUtils";
|
||||||
import { isCtrlDown, isShiftDown } from "src/utils/Utils";
|
|
||||||
|
|
||||||
declare const PLUGIN_VERSION:string;
|
declare const PLUGIN_VERSION:string;
|
||||||
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
|
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
|
||||||
@@ -258,6 +257,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
|||||||
className="Island App-menu__left scrollbar"
|
className="Island App-menu__left scrollbar"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: "350px",
|
maxHeight: "350px",
|
||||||
|
backgroundColor: "transparent",
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
"--padding": 2,
|
"--padding": 2,
|
||||||
display: this.state.minimized ? "none" : "block",
|
display: this.state.minimized ? "none" : "block",
|
||||||
@@ -467,12 +467,21 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
|||||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||||
isCtrlDown(e) ? "group=" : (isShiftDown(e) ? "area=" : "")
|
e[CTRL_OR_CMD] ? "group=" : (e.shiftKey ? "area=" : "")
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
icon={ICONS.copyElementLink}
|
icon={ICONS.copyElementLink}
|
||||||
view={this.props.view}
|
view={this.props.view}
|
||||||
/>
|
/>
|
||||||
|
<ActionButton
|
||||||
|
key={"import-svg"}
|
||||||
|
title={t("IMPORT_SVG")}
|
||||||
|
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
this.props.view.plugin.importSVGDialog.start(this.props.view);
|
||||||
|
}}
|
||||||
|
icon={ICONS.importSVG}
|
||||||
|
view={this.props.view}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{this.renderScriptButtons(false)}
|
{this.renderScriptButtons(false)}
|
||||||
@@ -533,21 +542,9 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
icon={
|
icon={
|
||||||
this.state.scriptIconMap[key].svgString ? (
|
this.state.scriptIconMap[key].svgString
|
||||||
<img
|
? stringToSVG(this.state.scriptIconMap[key].svgString)
|
||||||
src={`data:image/svg+xml,${encodeURIComponent(
|
: (
|
||||||
this.state.theme === "dark"
|
|
||||||
? this.state.scriptIconMap[key].svgString.replace(
|
|
||||||
"<svg ",
|
|
||||||
dark,
|
|
||||||
)
|
|
||||||
: this.state.scriptIconMap[key].svgString.replace(
|
|
||||||
"<svg ",
|
|
||||||
light,
|
|
||||||
),
|
|
||||||
)}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
ICONS.cog
|
ICONS.cog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -16,6 +16,7 @@ import {
|
|||||||
getEmbedFilename,
|
getEmbedFilename,
|
||||||
} from "./utils/FileUtils";
|
} from "./utils/FileUtils";
|
||||||
import {
|
import {
|
||||||
|
fragWithHTML,
|
||||||
setLeftHandedMode,
|
setLeftHandedMode,
|
||||||
} from "./utils/Utils";
|
} from "./utils/Utils";
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export interface ExcalidrawSettings {
|
|||||||
forceWrap: boolean;
|
forceWrap: boolean;
|
||||||
pageTransclusionCharLimit: number;
|
pageTransclusionCharLimit: number;
|
||||||
wordWrappingDefault: number;
|
wordWrappingDefault: number;
|
||||||
|
removeTransclusionQuoteSigns: boolean;
|
||||||
iframelyAllowed: boolean;
|
iframelyAllowed: boolean;
|
||||||
pngExportScale: number;
|
pngExportScale: number;
|
||||||
exportWithTheme: boolean;
|
exportWithTheme: boolean;
|
||||||
@@ -149,6 +151,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
forceWrap: false,
|
forceWrap: false,
|
||||||
pageTransclusionCharLimit: 200,
|
pageTransclusionCharLimit: 200,
|
||||||
wordWrappingDefault: 0,
|
wordWrappingDefault: 0,
|
||||||
|
removeTransclusionQuoteSigns: true,
|
||||||
iframelyAllowed: true,
|
iframelyAllowed: true,
|
||||||
pngExportScale: 1,
|
pngExportScale: 1,
|
||||||
exportWithTheme: true,
|
exportWithTheme: true,
|
||||||
@@ -196,9 +199,6 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
mathjaxSourceURL: "https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js"
|
mathjaxSourceURL: "https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js"
|
||||||
};
|
};
|
||||||
|
|
||||||
const fragWithHTML = (html: string) =>
|
|
||||||
createFragment((frag) => (frag.createDiv().innerHTML = html));
|
|
||||||
|
|
||||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||||
plugin: ExcalidrawPlugin;
|
plugin: ExcalidrawPlugin;
|
||||||
private requestEmbedUpdate: boolean = false;
|
private requestEmbedUpdate: boolean = false;
|
||||||
@@ -809,6 +809,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("QUOTE_TRANSCLUSION_REMOVE_NAME"))
|
||||||
|
.setDesc(fragWithHTML(t("QUOTE_TRANSCLUSION_REMOVE_DESC")))
|
||||||
|
.addToggle(toggle =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.removeTransclusionQuoteSigns)
|
||||||
|
.onChange(value => {
|
||||||
|
this.plugin.settings.removeTransclusionQuoteSigns = value;
|
||||||
|
this.requestEmbedUpdate = true;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("GET_URL_TITLE_NAME"))
|
.setName(t("GET_URL_TITLE_NAME"))
|
||||||
.setDesc(fragWithHTML(t("GET_URL_TITLE_DESC")))
|
.setDesc(fragWithHTML(t("GET_URL_TITLE_DESC")))
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import chroma from "chroma-js";
|
||||||
|
import { ExcalidrawElementBase } from "./elements/ExcalidrawElement";
|
||||||
|
|
||||||
|
export function hexWithAlpha(color: string, alpha: number): string {
|
||||||
|
return chroma(color).alpha(alpha).css();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function has(el: Element, attr: string): boolean {
|
||||||
|
return el.hasAttribute(attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(el: Element, attr: string, backup?: string): string {
|
||||||
|
return el.getAttribute(attr) || backup || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNum(el: Element, attr: string, backup?: number): number {
|
||||||
|
const numVal = Number(get(el, attr));
|
||||||
|
return numVal === NaN ? backup || 0 : numVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presAttrs = {
|
||||||
|
stroke: "stroke",
|
||||||
|
"stroke-opacity": "stroke-opacity",
|
||||||
|
"stroke-width": "stroke-width",
|
||||||
|
fill: "fill",
|
||||||
|
"fill-opacity": "fill-opacity",
|
||||||
|
opacity: "opacity",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type ExPartialElement = Partial<ExcalidrawElementBase>;
|
||||||
|
|
||||||
|
type AttrHandlerArgs = {
|
||||||
|
el: Element;
|
||||||
|
exVals: ExPartialElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PresAttrHandlers = {
|
||||||
|
[key in keyof typeof presAttrs]: (args: AttrHandlerArgs) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const attrHandlers: PresAttrHandlers = {
|
||||||
|
stroke: ({ el, exVals }) => {
|
||||||
|
const strokeColor = get(el, "stroke");
|
||||||
|
|
||||||
|
exVals.strokeColor = has(el, "stroke-opacity")
|
||||||
|
? hexWithAlpha(strokeColor, getNum(el, "stroke-opacity"))
|
||||||
|
: strokeColor;
|
||||||
|
},
|
||||||
|
|
||||||
|
"stroke-opacity": ({ el, exVals }) => {
|
||||||
|
exVals.strokeColor = hexWithAlpha(
|
||||||
|
get(el, "stroke", "#000000"),
|
||||||
|
getNum(el, "stroke-opacity"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
"stroke-width": ({ el, exVals }) => {
|
||||||
|
exVals.strokeWidth = getNum(el, "stroke-width");
|
||||||
|
},
|
||||||
|
|
||||||
|
fill: ({ el, exVals }) => {
|
||||||
|
const fill = get(el, `fill`);
|
||||||
|
|
||||||
|
exVals.backgroundColor = fill === "none" ? "#00000000" : fill;
|
||||||
|
},
|
||||||
|
|
||||||
|
"fill-opacity": ({ el, exVals }) => {
|
||||||
|
exVals.backgroundColor = hexWithAlpha(
|
||||||
|
get(el, "fill", "#000000"),
|
||||||
|
getNum(el, "fill-opacity"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
opacity: ({ el, exVals }) => {
|
||||||
|
exVals.opacity = getNum(el, "opacity", 100);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Presentation Attributes for SVG Elements:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation
|
||||||
|
export function presAttrsToElementValues(
|
||||||
|
el: Element,
|
||||||
|
): Partial<ExcalidrawElementBase> {
|
||||||
|
const exVals = [...el.attributes].reduce((exVals, attr) => {
|
||||||
|
const name = attr.name;
|
||||||
|
|
||||||
|
if (Object.keys(attrHandlers).includes(name)) {
|
||||||
|
attrHandlers[name as keyof PresAttrHandlers]({ el, exVals });
|
||||||
|
}
|
||||||
|
|
||||||
|
return exVals;
|
||||||
|
}, {} as ExPartialElement);
|
||||||
|
|
||||||
|
return exVals;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterAttrs = Partial<
|
||||||
|
Pick<ExcalidrawElementBase, "x" | "y" | "width" | "height">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function filterAttrsToElementValues(el: Element): FilterAttrs {
|
||||||
|
const filterVals: FilterAttrs = {};
|
||||||
|
|
||||||
|
if (has(el, "x")) {
|
||||||
|
filterVals.x = getNum(el, "x");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has(el, "y")) {
|
||||||
|
filterVals.y = getNum(el, "y");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has(el, "width")) {
|
||||||
|
filterVals.width = getNum(el, "width");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has(el, "height")) {
|
||||||
|
filterVals.height = getNum(el, "height");
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterVals;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pointsAttrToPoints(el: Element): number[][] {
|
||||||
|
let points: number[][] = [];
|
||||||
|
|
||||||
|
if (has(el, "points")) {
|
||||||
|
points = get(el, "points")
|
||||||
|
.split(" ")
|
||||||
|
.map((p) => p.split(",").map(parseFloat));
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { randomId, randomInteger } from "../utils";
|
||||||
|
|
||||||
|
import { ExcalidrawLinearElement, FillStyle, GroupId, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
|
||||||
|
export type Point = [number, number];
|
||||||
|
|
||||||
|
export type ExcalidrawElementBase = {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
strokeColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
fillStyle: FillStyle;
|
||||||
|
strokeWidth: number;
|
||||||
|
strokeStyle: StrokeStyle;
|
||||||
|
strokeSharpness: StrokeSharpness;
|
||||||
|
roughness: number;
|
||||||
|
opacity: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
angle: number;
|
||||||
|
/** Random integer used to seed shape generation so that the roughjs shape
|
||||||
|
doesn't differ across renders. */
|
||||||
|
seed: number;
|
||||||
|
/** Integer that is sequentially incremented on each change. Used to reconcile
|
||||||
|
elements during collaboration or when saving to server. */
|
||||||
|
version: number;
|
||||||
|
/** Random integer that is regenerated on each change.
|
||||||
|
Used for deterministic reconciliation of updates during collaboration,
|
||||||
|
in case the versions (see above) are identical. */
|
||||||
|
versionNonce: number;
|
||||||
|
isDeleted: boolean;
|
||||||
|
/** List of groups the element belongs to.
|
||||||
|
Ordered from deepest to shallowest. */
|
||||||
|
groupIds: GroupId[];
|
||||||
|
/** Ids of (linear) elements that are bound to this element. */
|
||||||
|
boundElementIds: ExcalidrawLinearElement["id"][] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawRectangle = ExcalidrawElementBase & {
|
||||||
|
type: "rectangle";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawLine = ExcalidrawElementBase & {
|
||||||
|
type: "line";
|
||||||
|
points: readonly Point[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawEllipse = ExcalidrawElementBase & {
|
||||||
|
type: "ellipse";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawGenericElement =
|
||||||
|
| ExcalidrawRectangle
|
||||||
|
| ExcalidrawEllipse
|
||||||
|
| ExcalidrawLine
|
||||||
|
| ExcalidrawDraw;
|
||||||
|
|
||||||
|
export type ExcalidrawDraw = ExcalidrawElementBase & {
|
||||||
|
type: "line";
|
||||||
|
points: readonly Point[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createExElement(): ExcalidrawElementBase {
|
||||||
|
return {
|
||||||
|
id: randomId(),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
roughness: 0,
|
||||||
|
opacity: 100,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
angle: 0,
|
||||||
|
seed: randomInteger(),
|
||||||
|
version: 0,
|
||||||
|
versionNonce: 0,
|
||||||
|
isDeleted: false,
|
||||||
|
groupIds: [],
|
||||||
|
boundElementIds: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExRect(): ExcalidrawRectangle {
|
||||||
|
return {
|
||||||
|
...createExElement(),
|
||||||
|
type: "rectangle",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExLine(): ExcalidrawLine {
|
||||||
|
return {
|
||||||
|
...createExElement(),
|
||||||
|
type: "line",
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExEllipse(): ExcalidrawEllipse {
|
||||||
|
return {
|
||||||
|
...createExElement(),
|
||||||
|
type: "ellipse",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExDraw(): ExcalidrawDraw {
|
||||||
|
return {
|
||||||
|
...createExElement(),
|
||||||
|
type: "line",
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
|
||||||
|
|
||||||
|
class ExcalidrawScene {
|
||||||
|
type = "excalidraw";
|
||||||
|
version = 2;
|
||||||
|
source = "https://excalidraw.com";
|
||||||
|
elements: ExcalidrawGenericElement[] = [];
|
||||||
|
|
||||||
|
constructor(elements:any = []) {
|
||||||
|
this.elements = elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
toExJSON(): any {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
elements: this.elements.map((el) => ({ ...el })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExcalidrawScene;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { randomId } from "../utils";
|
||||||
|
import { presAttrsToElementValues } from "../attributes";
|
||||||
|
import { ExcalidrawElementBase } from "../elements/ExcalidrawElement";
|
||||||
|
|
||||||
|
export function getGroupAttrs(groups: Group[]): any {
|
||||||
|
return groups.reduce((acc, { element }) => {
|
||||||
|
const elVals = presAttrsToElementValues(element);
|
||||||
|
|
||||||
|
return { ...acc, ...elVals };
|
||||||
|
}, {} as Partial<ExcalidrawElementBase>);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Group {
|
||||||
|
id = randomId();
|
||||||
|
|
||||||
|
element: Element;
|
||||||
|
|
||||||
|
constructor(element: Element) {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Group;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as path from "./path";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path,
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { RawElement } from "../../types";
|
||||||
|
import { getElementBoundaries } from "../utils";
|
||||||
|
import pathToPoints from "./utils/path-to-points";
|
||||||
|
|
||||||
|
const parse = (node: Element) => {
|
||||||
|
const data = node.getAttribute("d");
|
||||||
|
const backgroundColor = node.getAttribute("fill");
|
||||||
|
const strokeColor = node.getAttribute("stroke");
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data || "",
|
||||||
|
backgroundColor:
|
||||||
|
(backgroundColor !== "currentColor" && backgroundColor) || "transparent",
|
||||||
|
strokeColor: (strokeColor !== "currentColor" && strokeColor) || "#000000",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convert = (node: Element): RawElement[] => {
|
||||||
|
const { data, backgroundColor, strokeColor } = parse(node);
|
||||||
|
const elementsPoints = pathToPoints(data);
|
||||||
|
|
||||||
|
return elementsPoints.map((points) => {
|
||||||
|
const boundaries = getElementBoundaries(points);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "line",
|
||||||
|
roughness: 0,
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
points,
|
||||||
|
backgroundColor,
|
||||||
|
strokeColor,
|
||||||
|
...boundaries,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { safeNumber } from "../../../utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a point at a given section of a cubic bezier curve.
|
||||||
|
* This function only supports two dimensions curves
|
||||||
|
* @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
|
||||||
|
*/
|
||||||
|
const getPointOfCubicCurve = (
|
||||||
|
controlPoints: number[][],
|
||||||
|
section: number,
|
||||||
|
): number[] =>
|
||||||
|
Array.from({ length: 2 }).map((v, i) => {
|
||||||
|
const point =
|
||||||
|
controlPoints[0][i] * (1 - section) ** 3 +
|
||||||
|
3 * controlPoints[1][i] * section * (1 - section) ** 2 +
|
||||||
|
3 * controlPoints[2][i] * section ** 2 * (1 - section) +
|
||||||
|
controlPoints[3][i] * section ** 3;
|
||||||
|
|
||||||
|
return safeNumber(point);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a point at a given section of a quadratic bezier curve.
|
||||||
|
* This function only supports two dimensions curves
|
||||||
|
* @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
|
||||||
|
*/
|
||||||
|
const getPointOfQuadraticCurve = (
|
||||||
|
controlPoints: number[][],
|
||||||
|
section: number,
|
||||||
|
): number[] =>
|
||||||
|
Array.from({ length: 2 }).map((v, i) => {
|
||||||
|
const point =
|
||||||
|
controlPoints[0][i] * (1 - section) ** 2 +
|
||||||
|
2 * controlPoints[1][i] * section * (1 - section) +
|
||||||
|
controlPoints[2][i] * section ** 2;
|
||||||
|
|
||||||
|
return safeNumber(point);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of points for a cubic bézier curve.
|
||||||
|
* Starting point is not returned
|
||||||
|
*/
|
||||||
|
export const curveToPoints = (
|
||||||
|
type: "cubic" | "quadratic",
|
||||||
|
controlPoints: number[][],
|
||||||
|
nbPoints = 10,
|
||||||
|
): number[][] => {
|
||||||
|
if (nbPoints <= 0) {
|
||||||
|
throw new Error("Requested amount of points must be positive");
|
||||||
|
} else if (nbPoints > 100) {
|
||||||
|
nbPoints = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from({ length: nbPoints }, (value, index) => {
|
||||||
|
const section = safeNumber(((100 / nbPoints) * (index + 1)) / 100);
|
||||||
|
|
||||||
|
if (type === "cubic") {
|
||||||
|
return getPointOfCubicCurve(controlPoints, section);
|
||||||
|
} else if (type === "quadratic") {
|
||||||
|
return getPointOfQuadraticCurve(controlPoints, section);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid bézier curve type requested");
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const degreeToRadian = (degree: number): number => (degree * Math.PI) / 180;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get each possible ellipses center points given two points and ellipse radius
|
||||||
|
* @see https://math.stackexchange.com/questions/2240031/solving-an-equation-for-an-ellipse
|
||||||
|
*/
|
||||||
|
export const getEllipsesCenter = (
|
||||||
|
curX: number,
|
||||||
|
curY: number,
|
||||||
|
destX: number,
|
||||||
|
destY: number,
|
||||||
|
radiusX: number,
|
||||||
|
radiusY: number,
|
||||||
|
): number[][] => [
|
||||||
|
[
|
||||||
|
(curX + destX) / 2 +
|
||||||
|
((radiusX * (curY - destY)) / (2 * radiusY)) *
|
||||||
|
Math.sqrt(
|
||||||
|
4 /
|
||||||
|
((curX - destX) ** 2 / radiusX ** 2 +
|
||||||
|
(curY - destY) ** 2 / radiusY ** 2) -
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(curY + destY) / 2 -
|
||||||
|
((radiusY * (curX - destX)) / (2 * radiusX)) *
|
||||||
|
Math.sqrt(
|
||||||
|
4 /
|
||||||
|
((curX - destX) ** 2 / radiusX ** 2 +
|
||||||
|
(curY - destY) ** 2 / radiusY ** 2) -
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
(curX + destX) / 2 -
|
||||||
|
((radiusX * (curY - destY)) / (2 * radiusY)) *
|
||||||
|
Math.sqrt(
|
||||||
|
4 /
|
||||||
|
((curX - destX) ** 2 / radiusX ** 2 +
|
||||||
|
(curY - destY) ** 2 / radiusY ** 2) -
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(curY + destY) / 2 +
|
||||||
|
((radiusY * (curX - destX)) / (2 * radiusX)) *
|
||||||
|
Math.sqrt(
|
||||||
|
4 /
|
||||||
|
((curX - destX) ** 2 / radiusX ** 2 +
|
||||||
|
(curY - destY) ** 2 / radiusY ** 2) -
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get point of ellipse at given degree
|
||||||
|
*/
|
||||||
|
const getPointAtDegree = (
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
radiusX: number,
|
||||||
|
radiusY: number,
|
||||||
|
degree: number,
|
||||||
|
): number[] => [
|
||||||
|
Math.round(radiusX * Math.cos(degreeToRadian(degree)) + centerX),
|
||||||
|
Math.round(radiusY * Math.sin(degreeToRadian(degree)) + centerY),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all points of a given ellipse
|
||||||
|
*/
|
||||||
|
export const getEllipsePoints = (
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
radiusX: number,
|
||||||
|
radiusY: number,
|
||||||
|
): number[][] => {
|
||||||
|
const points: number[][] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 360; i += 1) {
|
||||||
|
const pointAtDegree = getPointAtDegree(
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radiusX,
|
||||||
|
radiusY,
|
||||||
|
i,
|
||||||
|
);
|
||||||
|
const existingPoint = points.find(
|
||||||
|
([x, y]) => x === pointAtDegree[0] && y === pointAtDegree[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingPoint) {
|
||||||
|
points.push(pointAtDegree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find ellipse arc given sweep parameter
|
||||||
|
*/
|
||||||
|
export const findArc = (
|
||||||
|
points: number[][],
|
||||||
|
sweep: boolean,
|
||||||
|
curX: number,
|
||||||
|
curY: number,
|
||||||
|
destX: number,
|
||||||
|
destY: number,
|
||||||
|
): number[][] => {
|
||||||
|
const indexCur = points.findIndex(
|
||||||
|
([x, y]) => x === Math.round(curX) && y === Math.round(curY),
|
||||||
|
);
|
||||||
|
const indexDest = points.findIndex(
|
||||||
|
([x, y]) => x === Math.round(destX) && y === Math.round(destY),
|
||||||
|
);
|
||||||
|
const arc = [];
|
||||||
|
const step = sweep ? -1 : 1;
|
||||||
|
|
||||||
|
for (let i = indexDest; true; i += step) {
|
||||||
|
arc.push(points[i]);
|
||||||
|
|
||||||
|
if (i === indexCur) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sweep && i === 0) {
|
||||||
|
i = points.length;
|
||||||
|
} else if (!sweep && i === points.length - 1) {
|
||||||
|
i = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arc.reverse();
|
||||||
|
};
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { PathCommand } from "../../../types";
|
||||||
|
import { safeNumber } from "../../../utils";
|
||||||
|
import { curveToPoints } from "./bezier";
|
||||||
|
import { findArc, getEllipsePoints, getEllipsesCenter } from "./ellipse";
|
||||||
|
|
||||||
|
const PATH_COMMANDS_REGEX =
|
||||||
|
/(?:([HhVv] *-?\d*(?:\.\d+)?)|([MmLlTt](?: *-?\d*(?:\.\d+)?(?:,| *)?){2})|([Cc](?: *-?\d*(?:\.\d+)?(?:,| *)?){6})|([QqSs](?: *-?\d*(?:\.\d+)?(?:,| *)?){4})|([Aa](?: *-?\d*(?:\.\d+)?(?:,| *)?){7})|(z|Z))/g;
|
||||||
|
const COMMAND_REGEX = /(?:[MmLlHhVvCcSsQqTtAaZz]|(-?\d+(?:\.\d+)?))/g;
|
||||||
|
|
||||||
|
const handleMoveToAndLineTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
parameters: number[],
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[] => {
|
||||||
|
if (isRelative) {
|
||||||
|
return [
|
||||||
|
currentPosition[0] + parameters[0],
|
||||||
|
currentPosition[1] + parameters[1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHorizontalLineTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
x: number,
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[] => {
|
||||||
|
if (isRelative) {
|
||||||
|
return [currentPosition[0] + x, currentPosition[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [x, currentPosition[1]];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerticalLineTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
y: number,
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[] => {
|
||||||
|
if (isRelative) {
|
||||||
|
return [currentPosition[0], currentPosition[1] + y];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [currentPosition[0], y];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCubicCurveTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
parameters: number[],
|
||||||
|
lastCommand: PathCommand,
|
||||||
|
isSimpleForm: boolean,
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[][] => {
|
||||||
|
const controlPoints = [currentPosition];
|
||||||
|
let inferredControlPoint;
|
||||||
|
|
||||||
|
if (isSimpleForm) {
|
||||||
|
inferredControlPoint = ["C", "c"].includes(lastCommand?.type)
|
||||||
|
? [
|
||||||
|
currentPosition[0] - (lastCommand.parameters[2] - currentPosition[0]),
|
||||||
|
currentPosition[1] - (lastCommand.parameters[3] - currentPosition[1]),
|
||||||
|
]
|
||||||
|
: currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelative) {
|
||||||
|
controlPoints.push(
|
||||||
|
inferredControlPoint || [
|
||||||
|
currentPosition[0] + parameters[0],
|
||||||
|
currentPosition[1] + parameters[1],
|
||||||
|
],
|
||||||
|
[currentPosition[0] + parameters[2], currentPosition[1] + parameters[3]],
|
||||||
|
[currentPosition[0] + parameters[4], currentPosition[1] + parameters[5]],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controlPoints.push(
|
||||||
|
inferredControlPoint || [parameters[0], parameters[1]],
|
||||||
|
[parameters[2], parameters[3]],
|
||||||
|
[parameters[4], parameters[5]],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return curveToPoints("cubic", controlPoints);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuadraticCurveTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
parameters: number[],
|
||||||
|
lastCommand: PathCommand,
|
||||||
|
isSimpleForm: boolean,
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[][] => {
|
||||||
|
const controlPoints = [currentPosition];
|
||||||
|
let inferredControlPoint;
|
||||||
|
|
||||||
|
if (isSimpleForm) {
|
||||||
|
inferredControlPoint = ["Q", "q"].includes(lastCommand?.type)
|
||||||
|
? [
|
||||||
|
currentPosition[0] - (lastCommand.parameters[0] - currentPosition[0]),
|
||||||
|
currentPosition[1] - (lastCommand.parameters[1] - currentPosition[1]),
|
||||||
|
]
|
||||||
|
: currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRelative) {
|
||||||
|
controlPoints.push(
|
||||||
|
inferredControlPoint || [
|
||||||
|
currentPosition[0] + parameters[0],
|
||||||
|
currentPosition[1] + parameters[1],
|
||||||
|
],
|
||||||
|
[currentPosition[0] + parameters[2], currentPosition[1] + parameters[3]],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controlPoints.push(inferredControlPoint || [parameters[0], parameters[1]], [
|
||||||
|
parameters[2],
|
||||||
|
parameters[3],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return curveToPoints("quadratic", controlPoints);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo handle arcs rotation
|
||||||
|
* @todo handle specific cases where only one ellipse can exist
|
||||||
|
*/
|
||||||
|
const handleArcTo = (
|
||||||
|
currentPosition: number[],
|
||||||
|
[radiusX, radiusY, , large, sweep, destX, destY]: number[],
|
||||||
|
isRelative: boolean,
|
||||||
|
): number[][] => {
|
||||||
|
destX = isRelative ? currentPosition[0] + destX : destX;
|
||||||
|
destY = isRelative ? currentPosition[1] + destY : destY;
|
||||||
|
|
||||||
|
const ellipsesCenter = getEllipsesCenter(
|
||||||
|
currentPosition[0],
|
||||||
|
currentPosition[1],
|
||||||
|
destX,
|
||||||
|
destY,
|
||||||
|
radiusX,
|
||||||
|
radiusY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ellipsesPoints = [
|
||||||
|
getEllipsePoints(
|
||||||
|
ellipsesCenter[0][0],
|
||||||
|
ellipsesCenter[0][1],
|
||||||
|
radiusX,
|
||||||
|
radiusY,
|
||||||
|
),
|
||||||
|
getEllipsePoints(
|
||||||
|
ellipsesCenter[1][0],
|
||||||
|
ellipsesCenter[1][1],
|
||||||
|
radiusX,
|
||||||
|
radiusY,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const arcs = [
|
||||||
|
findArc(
|
||||||
|
ellipsesPoints[0],
|
||||||
|
!!sweep,
|
||||||
|
currentPosition[0],
|
||||||
|
currentPosition[1],
|
||||||
|
destX,
|
||||||
|
destY,
|
||||||
|
),
|
||||||
|
findArc(
|
||||||
|
ellipsesPoints[1],
|
||||||
|
!!sweep,
|
||||||
|
currentPosition[0],
|
||||||
|
currentPosition[1],
|
||||||
|
destX,
|
||||||
|
destY,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const finalArc = arcs.reduce(
|
||||||
|
(arc, curArc) =>
|
||||||
|
(large && curArc.length > arc.length) ||
|
||||||
|
(!large && (!arc.length || curArc.length < arc.length))
|
||||||
|
? curArc
|
||||||
|
: arc,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return finalArc;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a SVG path data to list of points
|
||||||
|
*/
|
||||||
|
const pathToPoints = (path: string): number[][][] => {
|
||||||
|
const commands = path.match(PATH_COMMANDS_REGEX);
|
||||||
|
const elements = [];
|
||||||
|
const commandsHistory = [];
|
||||||
|
let currentPosition = [0, 0];
|
||||||
|
let points = [];
|
||||||
|
|
||||||
|
if (!commands?.length) {
|
||||||
|
throw new Error("No commands found in given path");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const command of commands) {
|
||||||
|
const lastCommand = commandsHistory[commandsHistory.length - 2];
|
||||||
|
const commandMatch = command.match(COMMAND_REGEX);
|
||||||
|
|
||||||
|
currentPosition = points[points.length - 1] || currentPosition;
|
||||||
|
|
||||||
|
if (commandMatch?.length) {
|
||||||
|
const commandType = commandMatch[0];
|
||||||
|
const parameters = commandMatch
|
||||||
|
.slice(1, commandMatch.length)
|
||||||
|
.map((parameter) => safeNumber(Number(parameter)));
|
||||||
|
const isRelative = commandType.toLowerCase() === commandType;
|
||||||
|
|
||||||
|
commandsHistory.push({
|
||||||
|
type: commandType,
|
||||||
|
parameters,
|
||||||
|
isRelative,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (commandType) {
|
||||||
|
case "M":
|
||||||
|
case "m":
|
||||||
|
case "L":
|
||||||
|
case "l":
|
||||||
|
points.push(
|
||||||
|
handleMoveToAndLineTo(currentPosition, parameters, isRelative),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "H":
|
||||||
|
case "h":
|
||||||
|
points.push(
|
||||||
|
handleHorizontalLineTo(currentPosition, parameters[0], isRelative),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "V":
|
||||||
|
case "v":
|
||||||
|
points.push(
|
||||||
|
handleVerticalLineTo(currentPosition, parameters[0], isRelative),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "C":
|
||||||
|
case "c":
|
||||||
|
case "S":
|
||||||
|
case "s":
|
||||||
|
points.push(
|
||||||
|
...handleCubicCurveTo(
|
||||||
|
currentPosition,
|
||||||
|
parameters,
|
||||||
|
lastCommand,
|
||||||
|
["S", "s"].includes(commandType),
|
||||||
|
isRelative,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "Q":
|
||||||
|
case "q":
|
||||||
|
case "T":
|
||||||
|
case "t":
|
||||||
|
points.push(
|
||||||
|
...handleQuadraticCurveTo(
|
||||||
|
currentPosition,
|
||||||
|
parameters,
|
||||||
|
lastCommand,
|
||||||
|
["T", "t"].includes(commandType),
|
||||||
|
isRelative,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "A":
|
||||||
|
case "a":
|
||||||
|
points.push(...handleArcTo(currentPosition, parameters, isRelative));
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "Z":
|
||||||
|
case "z":
|
||||||
|
if (points.length) {
|
||||||
|
if (
|
||||||
|
currentPosition[0] !== points[0][0] ||
|
||||||
|
currentPosition[1] !== points[0][1]
|
||||||
|
) {
|
||||||
|
points.push(points[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
points = [];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.error("Unsupported command provided will be ignored:", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.length === 0 && points.length) {
|
||||||
|
elements.push(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pathToPoints;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ElementBoundaries } from "../types";
|
||||||
|
|
||||||
|
export const getElementBoundaries = (points: number[][]): ElementBoundaries => {
|
||||||
|
const { x, y } = points.reduce(
|
||||||
|
(boundaries, [x, y]) => {
|
||||||
|
if (x < boundaries.x.min) {
|
||||||
|
boundaries.x.min = x;
|
||||||
|
}
|
||||||
|
if (x > boundaries.x.max) {
|
||||||
|
boundaries.x.max = x;
|
||||||
|
}
|
||||||
|
if (y < boundaries.y.min) {
|
||||||
|
boundaries.y.min = y;
|
||||||
|
}
|
||||||
|
if (y > boundaries.y.max) {
|
||||||
|
boundaries.y.max = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundaries;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: {
|
||||||
|
min: Infinity,
|
||||||
|
max: 0,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: Infinity,
|
||||||
|
max: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: x.min,
|
||||||
|
y: y.min,
|
||||||
|
width: x.max - x.min,
|
||||||
|
height: y.max - y.min,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import ExcalidrawScene from "./elements/ExcalidrawScene";
|
||||||
|
import Group from "./elements/Group";
|
||||||
|
import { createTreeWalker, walk } from "./walker";
|
||||||
|
|
||||||
|
export type ConversionResult = {
|
||||||
|
hasErrors: boolean;
|
||||||
|
errors: NodeListOf<Element> | null;
|
||||||
|
content: any; // Serialized Excalidraw JSON
|
||||||
|
};
|
||||||
|
|
||||||
|
export const svgToExcalidraw = (svgString: string): ConversionResult => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
|
||||||
|
|
||||||
|
// was there a parsing error?
|
||||||
|
const errorsElements = svgDOM.querySelectorAll("parsererror");
|
||||||
|
const hasErrors = errorsElements.length > 0;
|
||||||
|
let content = null;
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error(
|
||||||
|
"There were errors while parsing the given SVG: ",
|
||||||
|
[...errorsElements].map((el) => el.innerHTML),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const tw = createTreeWalker(svgDOM);
|
||||||
|
const scene = new ExcalidrawScene();
|
||||||
|
const groups: Group[] = [];
|
||||||
|
|
||||||
|
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
|
||||||
|
|
||||||
|
content = scene.elements; //scene.toExJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasErrors,
|
||||||
|
errors: hasErrors ? errorsElements : null,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Original source https://github.com/excalidraw/svg-to-excalidraw. Last commit: https://github.com/excalidraw/svg-to-excalidraw/commit/6f6e4b7269c4194b56cf7517a8357ba73be12a3a
|
||||||
|
Embedded into the project instead of using an import because compiled file size difference (smaller this way). Also the svg-to-excalidraw package has not been maintained for over a year, thus I don't expect to miss out on frequent updates
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import Group from "./elements/Group";
|
||||||
|
import { vec3, mat4 } from "gl-matrix";
|
||||||
|
|
||||||
|
/*
|
||||||
|
SVG transform attr is a bit strange in that it can accept traditional
|
||||||
|
css transform string (at least per spec) as well as a it's own "unitless"
|
||||||
|
version of transform functions.
|
||||||
|
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
|
||||||
|
*/
|
||||||
|
|
||||||
|
const transformFunctions = {
|
||||||
|
matrix: "matrix",
|
||||||
|
matrix3d: "matrix3d",
|
||||||
|
perspective: "perspective",
|
||||||
|
rotate: "rotate",
|
||||||
|
rotate3d: "rotate3d",
|
||||||
|
rotateX: "rotateX",
|
||||||
|
rotateY: "rotateY",
|
||||||
|
rotateZ: "rotateZ",
|
||||||
|
scale: "scale",
|
||||||
|
scale3d: "scale3d",
|
||||||
|
scaleX: "scaleX",
|
||||||
|
scaleY: "scaleY",
|
||||||
|
scaleZ: "scaleZ",
|
||||||
|
skew: "skew",
|
||||||
|
skewX: "skewX",
|
||||||
|
skewY: "skewY",
|
||||||
|
translate: "translate",
|
||||||
|
translate3d: "translate3d",
|
||||||
|
translateX: "translateX",
|
||||||
|
translateY: "translateY",
|
||||||
|
translateZ: "translateZ",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const transformFunctionsArr = Object.keys(transformFunctions);
|
||||||
|
|
||||||
|
// type Transform
|
||||||
|
|
||||||
|
type TransformFuncValue = {
|
||||||
|
value: string;
|
||||||
|
unit: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TransformFunc = {
|
||||||
|
type: keyof typeof transformFunctions;
|
||||||
|
values: TransformFuncValue[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultUnits = {
|
||||||
|
matrix: "",
|
||||||
|
matrix3d: "",
|
||||||
|
perspective: "perspective",
|
||||||
|
rotate: "deg",
|
||||||
|
rotate3d: "deg",
|
||||||
|
rotateX: "deg",
|
||||||
|
rotateY: "deg",
|
||||||
|
rotateZ: "deg",
|
||||||
|
scale: "",
|
||||||
|
scale3d: "",
|
||||||
|
scaleX: "",
|
||||||
|
scaleY: "",
|
||||||
|
scaleZ: "",
|
||||||
|
skew: "skew",
|
||||||
|
skewX: "deg",
|
||||||
|
skewY: "deg",
|
||||||
|
translate: "px",
|
||||||
|
translate3d: "px",
|
||||||
|
translateX: "px",
|
||||||
|
translateY: "px",
|
||||||
|
translateZ: "px",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert between possible svg transform attribute values to css transform attribute values.
|
||||||
|
const svgTransformToCSSTransform = (svgTransformStr: string): string => {
|
||||||
|
// Create transform function string "chunks", e.g "rotate(90deg)"
|
||||||
|
const tFuncs = svgTransformStr.match(/(\w+)\(([^)]*)\)/g);
|
||||||
|
if (!tFuncs) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tFuncValues: TransformFunc[] = tFuncs.map((tFuncStr): TransformFunc => {
|
||||||
|
const type = tFuncStr.split("(")[0] as keyof typeof transformFunctions;
|
||||||
|
if (!type) {
|
||||||
|
throw new Error("Unable to find transform name");
|
||||||
|
}
|
||||||
|
if (!transformFunctionsArr.includes(type)) {
|
||||||
|
throw new Error(`transform function name "${type}" is not valid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the arg/props of the transform function, e.g "90deg".
|
||||||
|
const tFuncParts = tFuncStr.match(/([-+]?[0-9]*\.?[0-9]+)([a-z])*/g);
|
||||||
|
if (!tFuncParts) {
|
||||||
|
return { type, values: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = tFuncParts.map((a): TransformFuncValue => {
|
||||||
|
// Separate the arg value and unit. e.g ["90", "deg"]
|
||||||
|
const [value, unit] = a.matchAll(/([-+]?[0-9]*\.?[0-9]+)|([a-z])*/g);
|
||||||
|
|
||||||
|
return {
|
||||||
|
unit: unit[0] || defaultUnits[type],
|
||||||
|
value: value[0],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not supporting x, y args of svg rotate transform yet...
|
||||||
|
if (values && type === "rotate" && values?.length > 1) {
|
||||||
|
values = [values[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a string of transform functions that can be set as a CSS Transform.
|
||||||
|
const csstransformStr = tFuncValues
|
||||||
|
.map(({ type, values }) => {
|
||||||
|
const valStr = values
|
||||||
|
.map(({ unit, value }) => `${value}${unit}`)
|
||||||
|
.join(", ");
|
||||||
|
return `${type}(${valStr})`;
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return csstransformStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDOMMatrixFromSVGStr = (
|
||||||
|
svgTransformStr: string,
|
||||||
|
): DOMMatrix => {
|
||||||
|
const cssTransformStr = svgTransformToCSSTransform(svgTransformStr);
|
||||||
|
|
||||||
|
return new DOMMatrix(cssTransformStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getElementMatrix(el: Element): mat4 {
|
||||||
|
if (el.hasAttribute("transform")) {
|
||||||
|
const elMat = new DOMMatrix(
|
||||||
|
svgTransformToCSSTransform(el.getAttribute("transform") || ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
return mat4.multiply(mat4.create(), mat4.create(), elMat.toFloat32Array());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mat4.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTransformMatrix(el: Element, groups: Group[]): mat4 {
|
||||||
|
const accumMat = groups
|
||||||
|
.map(({ element }) => getElementMatrix(element))
|
||||||
|
.concat([getElementMatrix(el)])
|
||||||
|
.reduce((acc, mat) => mat4.multiply(acc, acc, mat), mat4.create());
|
||||||
|
|
||||||
|
return accumMat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformPoints(
|
||||||
|
points: number[][],
|
||||||
|
transform: mat4,
|
||||||
|
): [number, number][] {
|
||||||
|
return points.map(([x, y]) => {
|
||||||
|
const [newX, newY] = vec3.transformMat4(
|
||||||
|
vec3.create(),
|
||||||
|
vec3.fromValues(x, y, 1),
|
||||||
|
transform,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [newX, newY];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
|
||||||
|
export type PathCommand = {
|
||||||
|
type: string;
|
||||||
|
parameters: number[];
|
||||||
|
isRelative: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RawElement = {
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
points: number[][];
|
||||||
|
backgroundColor: string;
|
||||||
|
strokeColor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ElementBoundaries = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* from Excalidraw codebase */
|
||||||
|
|
||||||
|
// 1-based in case we ever do `if(element.fontFamily)`
|
||||||
|
export const FONT_FAMILY = {
|
||||||
|
1: "Virgil",
|
||||||
|
2: "Helvetica",
|
||||||
|
3: "Cascadia",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export declare type RoughPoint = [number, number];
|
||||||
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
|
export declare type Line = [Point, Point];
|
||||||
|
export interface Rectangle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type _ExcalidrawElementBase = Readonly<{
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
strokeColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
fillStyle: FillStyle;
|
||||||
|
strokeWidth: number;
|
||||||
|
strokeStyle: StrokeStyle;
|
||||||
|
strokeSharpness: StrokeSharpness;
|
||||||
|
roughness: number;
|
||||||
|
opacity: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
angle: number;
|
||||||
|
/** Random integer used to seed shape generation so that the roughjs shape
|
||||||
|
doesn't differ across renders. */
|
||||||
|
seed: number;
|
||||||
|
/** Integer that is sequentially incremented on each change. Used to reconcile
|
||||||
|
elements during collaboration or when saving to server. */
|
||||||
|
version: number;
|
||||||
|
/** Random integer that is regenerated on each change.
|
||||||
|
Used for deterministic reconciliation of updates during collaboration,
|
||||||
|
in case the versions (see above) are identical. */
|
||||||
|
versionNonce: number;
|
||||||
|
isDeleted: boolean;
|
||||||
|
/** List of groups the element belongs to.
|
||||||
|
Ordered from deepest to shallowest. */
|
||||||
|
groupIds: readonly GroupId[];
|
||||||
|
/** Ids of (linear) elements that are bound to this element. */
|
||||||
|
boundElementIds: readonly ExcalidrawLinearElement["id"][] | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||||
|
type: "selection";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
|
||||||
|
type: "rectangle";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
||||||
|
type: "diamond";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
||||||
|
type: "ellipse";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are elements that don't have any additional properties.
|
||||||
|
*/
|
||||||
|
export type ExcalidrawGenericElement =
|
||||||
|
| ExcalidrawSelectionElement
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawDiamondElement
|
||||||
|
| ExcalidrawEllipseElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
|
* no computed data. The list of all ExcalidrawElements should be shareable
|
||||||
|
* between peers and contain no state local to the peer.
|
||||||
|
*/
|
||||||
|
export type _ExcalidrawElement =
|
||||||
|
| ExcalidrawGenericElement
|
||||||
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
|
isDeleted: false;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Random } from "roughjs/bin/math";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { Point } from "./elements/ExcalidrawElement";
|
||||||
|
|
||||||
|
const random = new Random(Date.now());
|
||||||
|
|
||||||
|
export const randomInteger = (): number => Math.floor(random.next() * 2 ** 31);
|
||||||
|
|
||||||
|
export const randomId = (): string => nanoid();
|
||||||
|
|
||||||
|
export const safeNumber = (number: number): number => Number(number.toFixed(2));
|
||||||
|
|
||||||
|
export function dimensionsFromPoints(points: number[][]): number[] {
|
||||||
|
const xCoords = points.map(([x]) => x);
|
||||||
|
const yCoords = points.map(([, y]) => y);
|
||||||
|
|
||||||
|
const minX = Math.min(...xCoords);
|
||||||
|
const minY = Math.min(...yCoords);
|
||||||
|
const maxX = Math.max(...xCoords);
|
||||||
|
const maxY = Math.max(...yCoords);
|
||||||
|
|
||||||
|
return [maxX - minX, maxY - minY];
|
||||||
|
}
|
||||||
|
|
||||||
|
// winding order is clockwise values is positive, counter clockwise if negative.
|
||||||
|
export function getWindingOrder(
|
||||||
|
points: Point[],
|
||||||
|
): "clockwise" | "counterclockwise" {
|
||||||
|
const total = points.reduce((acc, [x1, y1], idx, arr) => {
|
||||||
|
const p2 = arr[idx + 1];
|
||||||
|
const x2 = p2 ? p2[0] : 0;
|
||||||
|
const y2 = p2 ? p2[1] : 0;
|
||||||
|
|
||||||
|
const e = (x2 - x1) * (y2 + y1);
|
||||||
|
|
||||||
|
return e + acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return total > 0 ? "clockwise" : "counterclockwise";
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
import { mat4 } from "gl-matrix";
|
||||||
|
import { dimensionsFromPoints } from "./utils";
|
||||||
|
import ExcalidrawScene from "./elements/ExcalidrawScene";
|
||||||
|
import Group, { getGroupAttrs } from "./elements/Group";
|
||||||
|
import {
|
||||||
|
ExcalidrawElementBase,
|
||||||
|
ExcalidrawRectangle,
|
||||||
|
ExcalidrawEllipse,
|
||||||
|
ExcalidrawLine,
|
||||||
|
ExcalidrawDraw,
|
||||||
|
createExRect,
|
||||||
|
createExEllipse,
|
||||||
|
createExLine,
|
||||||
|
createExDraw,
|
||||||
|
Point,
|
||||||
|
} from "./elements/ExcalidrawElement";
|
||||||
|
import {
|
||||||
|
presAttrsToElementValues,
|
||||||
|
filterAttrsToElementValues,
|
||||||
|
pointsAttrToPoints,
|
||||||
|
has,
|
||||||
|
get,
|
||||||
|
getNum,
|
||||||
|
} from "./attributes";
|
||||||
|
import { getTransformMatrix, transformPoints } from "./transform";
|
||||||
|
import { pointsOnPath } from "points-on-path";
|
||||||
|
import { randomId, getWindingOrder } from "./utils";
|
||||||
|
|
||||||
|
const SUPPORTED_TAGS = [
|
||||||
|
"svg",
|
||||||
|
"path",
|
||||||
|
"g",
|
||||||
|
"use",
|
||||||
|
"circle",
|
||||||
|
"ellipse",
|
||||||
|
"rect",
|
||||||
|
"polyline",
|
||||||
|
"polygon",
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeValidator = (node: Element): number => {
|
||||||
|
if (SUPPORTED_TAGS.includes(node.tagName)) {
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createTreeWalker(dom: Node): TreeWalker {
|
||||||
|
return document.createTreeWalker(dom, NodeFilter.SHOW_ALL, {
|
||||||
|
acceptNode: nodeValidator,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalkerArgs = {
|
||||||
|
root: Document;
|
||||||
|
tw: TreeWalker;
|
||||||
|
scene: ExcalidrawScene;
|
||||||
|
groups: Group[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const presAttrs = (
|
||||||
|
el: Element,
|
||||||
|
groups: Group[],
|
||||||
|
): Partial<ExcalidrawElementBase> => {
|
||||||
|
return {
|
||||||
|
...getGroupAttrs(groups),
|
||||||
|
...presAttrsToElementValues(el),
|
||||||
|
...filterAttrsToElementValues(el),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const skippedUseAttrs = ["id"];
|
||||||
|
const allwaysPassedUseAttrs = [
|
||||||
|
"x",
|
||||||
|
"y",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"href",
|
||||||
|
"xlink:href",
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
"Most attributes on use do not override those already on the element
|
||||||
|
referenced by use. (This differs from how CSS style attributes override
|
||||||
|
those set 'earlier' in the cascade). Only the attributes x, y, width,
|
||||||
|
height and href on the use element will override those set on the
|
||||||
|
referenced element. However, any other attributes not set on the referenced
|
||||||
|
element will be applied to the use element."
|
||||||
|
|
||||||
|
Situation 1: Attr is set on defEl, NOT on useEl
|
||||||
|
- result: use defEl attr
|
||||||
|
Situation 2: Attr is on useEl, NOT on defEl
|
||||||
|
- result: use the useEl attr
|
||||||
|
Situation 3: Attr is on both useEl and defEl
|
||||||
|
- result: use the defEl attr (Unless x, y, width, height, href, xlink:href)
|
||||||
|
*/
|
||||||
|
const getDefElWithCorrectAttrs = (defEl: Element, useEl: Element): Element => {
|
||||||
|
const finalEl = [...useEl.attributes].reduce((el, attr) => {
|
||||||
|
if (skippedUseAttrs.includes(attr.value)) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does defEl have the attr? If so, use it, else use the useEl attr
|
||||||
|
if (
|
||||||
|
!defEl.hasAttribute(attr.name) ||
|
||||||
|
allwaysPassedUseAttrs.includes(attr.name)
|
||||||
|
) {
|
||||||
|
el.setAttribute(attr.name, useEl.getAttribute(attr.name) || "");
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}, defEl.cloneNode() as Element);
|
||||||
|
|
||||||
|
return finalEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const walkers = {
|
||||||
|
svg: (args: WalkerArgs) => {
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
g: (args: WalkerArgs) => {
|
||||||
|
const nextArgs = {
|
||||||
|
...args,
|
||||||
|
tw: createTreeWalker(args.tw.currentNode),
|
||||||
|
groups: [...args.groups, new Group(args.tw.currentNode as Element)],
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(nextArgs, nextArgs.tw.nextNode());
|
||||||
|
|
||||||
|
walk(args, args.tw.nextSibling());
|
||||||
|
},
|
||||||
|
|
||||||
|
use: (args: WalkerArgs) => {
|
||||||
|
const { root, tw, scene } = args;
|
||||||
|
const useEl = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const id = useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("unable to get id of use element");
|
||||||
|
}
|
||||||
|
|
||||||
|
const defEl = root.querySelector(id);
|
||||||
|
|
||||||
|
if (!defEl) {
|
||||||
|
throw new Error(`unable to find def element with id: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempScene = new ExcalidrawScene();
|
||||||
|
|
||||||
|
const finalEl = getDefElWithCorrectAttrs(defEl, useEl);
|
||||||
|
|
||||||
|
walk(
|
||||||
|
{
|
||||||
|
...args,
|
||||||
|
scene: tempScene,
|
||||||
|
tw: createTreeWalker(finalEl),
|
||||||
|
},
|
||||||
|
finalEl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const exEl = tempScene.elements.pop();
|
||||||
|
|
||||||
|
if (exEl) {
|
||||||
|
scene.elements.push(exEl);
|
||||||
|
//throw new Error("Unable to create ex element");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
circle: (args: WalkerArgs): void => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const r = getNum(el, "r", 0);
|
||||||
|
const d = r * 2;
|
||||||
|
const x = getNum(el, "x", 0) + getNum(el, "cx", 0) - r;
|
||||||
|
const y = getNum(el, "y", 0) + getNum(el, "cy", 0) - r;
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const m = mat4.fromValues(d, 0, 0, 0, 0, d, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||||
|
|
||||||
|
const result = mat4.multiply(mat4.create(), mat, m);
|
||||||
|
|
||||||
|
const circle: ExcalidrawEllipse = {
|
||||||
|
...createExEllipse(),
|
||||||
|
...presAttrs(el, groups),
|
||||||
|
x: result[12],
|
||||||
|
y: result[13],
|
||||||
|
width: result[0],
|
||||||
|
height: result[5],
|
||||||
|
groupIds: groups.map((g) => g.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.elements.push(circle);
|
||||||
|
|
||||||
|
walk(args, tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
ellipse: (args: WalkerArgs): void => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const rx = getNum(el, "rx", 0);
|
||||||
|
const ry = getNum(el, "ry", 0);
|
||||||
|
const cx = getNum(el, "cx", 0);
|
||||||
|
const cy = getNum(el, "cy", 0);
|
||||||
|
const x = getNum(el, "x", 0) + cx - rx;
|
||||||
|
const y = getNum(el, "y", 0) + cy - ry;
|
||||||
|
const w = rx * 2;
|
||||||
|
const h = ry * 2;
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||||
|
|
||||||
|
const result = mat4.multiply(mat4.create(), mat, m);
|
||||||
|
|
||||||
|
const ellipse: ExcalidrawEllipse = {
|
||||||
|
...createExEllipse(),
|
||||||
|
...presAttrs(el, groups),
|
||||||
|
x: result[12],
|
||||||
|
y: result[13],
|
||||||
|
width: result[0],
|
||||||
|
height: result[5],
|
||||||
|
groupIds: groups.map((g) => g.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.elements.push(ellipse);
|
||||||
|
|
||||||
|
walk(args, tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
line: (args: WalkerArgs) => {
|
||||||
|
// unimplemented
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
polygon: (args: WalkerArgs) => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const points = pointsAttrToPoints(el);
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
const transformedPoints = transformPoints(points, mat);
|
||||||
|
|
||||||
|
// The first point needs to be 0, 0, and all following points
|
||||||
|
// are relative to the first point.
|
||||||
|
const x = transformedPoints[0][0];
|
||||||
|
const y = transformedPoints[0][1];
|
||||||
|
|
||||||
|
const relativePoints = transformedPoints.map(([_x, _y]) => [
|
||||||
|
_x - x,
|
||||||
|
_y - y,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [width, height] = dimensionsFromPoints(relativePoints);
|
||||||
|
|
||||||
|
const line: ExcalidrawLine = {
|
||||||
|
...createExLine(),
|
||||||
|
...getGroupAttrs(groups),
|
||||||
|
...presAttrsToElementValues(el),
|
||||||
|
points: relativePoints.concat([[0, 0]]),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.elements.push(line);
|
||||||
|
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
polyline: (args: WalkerArgs) => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
const points = pointsAttrToPoints(el);
|
||||||
|
const transformedPoints = transformPoints(points, mat);
|
||||||
|
|
||||||
|
// The first point needs to be 0, 0, and all following points
|
||||||
|
// are relative to the first point.
|
||||||
|
const x = transformedPoints[0][0];
|
||||||
|
const y = transformedPoints[0][1];
|
||||||
|
|
||||||
|
const relativePoints = transformedPoints.map(([_x, _y]) => [
|
||||||
|
_x - x,
|
||||||
|
_y - y,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [width, height] = dimensionsFromPoints(relativePoints);
|
||||||
|
|
||||||
|
const hasFill = has(el, "fill");
|
||||||
|
const fill = get(el, "fill");
|
||||||
|
|
||||||
|
const shouldFill = !hasFill || (hasFill && fill !== "none");
|
||||||
|
|
||||||
|
const line: ExcalidrawLine = {
|
||||||
|
...createExLine(),
|
||||||
|
...getGroupAttrs(groups),
|
||||||
|
...presAttrsToElementValues(el),
|
||||||
|
points: relativePoints.concat(shouldFill ? [[0, 0]] : []),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.elements.push(line);
|
||||||
|
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
rect: (args: WalkerArgs) => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const x = getNum(el, "x", 0);
|
||||||
|
const y = getNum(el, "y", 0);
|
||||||
|
const w = getNum(el, "width", 0);
|
||||||
|
const h = getNum(el, "height", 0);
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||||
|
|
||||||
|
const result = mat4.multiply(mat4.create(), mat, m);
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE: Currently there doesn't seem to be a way to specify the border
|
||||||
|
radius of a rect within Excalidraw. This means that attributes
|
||||||
|
rx and ry can't be used.
|
||||||
|
*/
|
||||||
|
const isRound = el.hasAttribute("rx") || el.hasAttribute("ry");
|
||||||
|
|
||||||
|
const rect: ExcalidrawRectangle = {
|
||||||
|
...createExRect(),
|
||||||
|
...presAttrs(el, groups),
|
||||||
|
x: result[12],
|
||||||
|
y: result[13],
|
||||||
|
width: result[0],
|
||||||
|
height: result[5],
|
||||||
|
strokeSharpness: isRound ? "round" : "sharp",
|
||||||
|
};
|
||||||
|
|
||||||
|
scene.elements.push(rect);
|
||||||
|
|
||||||
|
walk(args, args.tw.nextNode());
|
||||||
|
},
|
||||||
|
|
||||||
|
path: (args: WalkerArgs) => {
|
||||||
|
const { tw, scene, groups } = args;
|
||||||
|
const el = tw.currentNode as Element;
|
||||||
|
|
||||||
|
const mat = getTransformMatrix(el, groups);
|
||||||
|
|
||||||
|
const points = pointsOnPath(get(el, "d"));
|
||||||
|
|
||||||
|
const fillColor = get(el, "fill", "black");
|
||||||
|
const fillRule = get(el, "fill-rule", "nonzero");
|
||||||
|
|
||||||
|
let elements: ExcalidrawDraw[] = [];
|
||||||
|
let localGroup = randomId();
|
||||||
|
|
||||||
|
switch (fillRule) {
|
||||||
|
case "nonzero":
|
||||||
|
let initialWindingOrder = "clockwise";
|
||||||
|
|
||||||
|
elements = points.map((pointArr, idx): ExcalidrawDraw => {
|
||||||
|
const tPoints: Point[] = transformPoints(pointArr, mat4.clone(mat));
|
||||||
|
const x = tPoints[0][0];
|
||||||
|
const y = tPoints[0][1];
|
||||||
|
|
||||||
|
const [width, height] = dimensionsFromPoints(tPoints);
|
||||||
|
|
||||||
|
const relativePoints = tPoints.map(
|
||||||
|
([_x, _y]): Point => [_x - x, _y - y],
|
||||||
|
);
|
||||||
|
|
||||||
|
const windingOrder = getWindingOrder(relativePoints);
|
||||||
|
if (idx === 0) {
|
||||||
|
initialWindingOrder = windingOrder;
|
||||||
|
localGroup = randomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundColor = fillColor;
|
||||||
|
if (initialWindingOrder !== windingOrder) {
|
||||||
|
backgroundColor = "#FFFFFF";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...createExDraw(),
|
||||||
|
strokeWidth: 0,
|
||||||
|
strokeColor: "#00000000",
|
||||||
|
...presAttrs(el, groups),
|
||||||
|
points: relativePoints,
|
||||||
|
backgroundColor,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x: x + getNum(el, "x", 0),
|
||||||
|
y: y + getNum(el, "y", 0),
|
||||||
|
groupIds: [localGroup],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "evenodd":
|
||||||
|
elements = points.map((pointArr, idx): ExcalidrawDraw => {
|
||||||
|
const tPoints: Point[] = transformPoints(pointArr, mat4.clone(mat));
|
||||||
|
const x = tPoints[0][0];
|
||||||
|
const y = tPoints[0][1];
|
||||||
|
|
||||||
|
const [width, height] = dimensionsFromPoints(tPoints);
|
||||||
|
|
||||||
|
const relativePoints = tPoints.map(
|
||||||
|
([_x, _y]): Point => [_x - x, _y - y],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idx === 0) {
|
||||||
|
localGroup = randomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...createExDraw(),
|
||||||
|
...presAttrs(el, groups),
|
||||||
|
points: relativePoints,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x: x + getNum(el, "x", 0),
|
||||||
|
y: y + getNum(el, "y", 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.elements = scene.elements.concat(elements);
|
||||||
|
|
||||||
|
walk(args, tw.nextNode());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function walk(args: WalkerArgs, nextNode: Node | null): void {
|
||||||
|
if (!nextNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeName = nextNode.nodeName as keyof typeof walkers;
|
||||||
|
if (walkers[nodeName]) {
|
||||||
|
walkers[nodeName](args);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, NonDeletedExcalidrawElement, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawImageElement, FileId, FillStyle, NonDeletedExcalidrawElement, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||||
import { Point } from "@zsviczian/excalidraw/types/types";
|
import { Point } from "@zsviczian/excalidraw/types/types";
|
||||||
import { TFile, WorkspaceLeaf } from "obsidian";
|
import { TFile, WorkspaceLeaf } from "obsidian";
|
||||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||||
@@ -223,6 +223,7 @@ export interface ExcalidrawAutomateInterface {
|
|||||||
//verifyMinimumPluginVersion returns true if plugin version is >= than required
|
//verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||||
//recommended use:
|
//recommended use:
|
||||||
//if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
|
//if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
|
||||||
|
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>;
|
||||||
verifyMinimumPluginVersion(requiredVersion: string): boolean;
|
verifyMinimumPluginVersion(requiredVersion: string): boolean;
|
||||||
isExcalidrawView(view: any): boolean;
|
isExcalidrawView(view: any): boolean;
|
||||||
selectElementsInView(elements: ExcalidrawElement[]): void; //sets selection in view
|
selectElementsInView(elements: ExcalidrawElement[]): void; //sets selection in view
|
||||||
|
|||||||
+23
-36
@@ -16,7 +16,6 @@ import {
|
|||||||
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
||||||
FRONTMATTER_KEY_EXPORT_PNGSCALE,
|
FRONTMATTER_KEY_EXPORT_PNGSCALE,
|
||||||
FRONTMATTER_KEY_EXPORT_PADDING,
|
FRONTMATTER_KEY_EXPORT_PADDING,
|
||||||
CTRL_OR_CMD,
|
|
||||||
} from "../Constants";
|
} from "../Constants";
|
||||||
import ExcalidrawPlugin from "../main";
|
import ExcalidrawPlugin from "../main";
|
||||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||||
@@ -24,7 +23,6 @@ import { ExportSettings } from "../ExcalidrawView";
|
|||||||
import { compressToBase64, decompressFromBase64 } from "lz-string";
|
import { compressToBase64, decompressFromBase64 } from "lz-string";
|
||||||
import { getIMGFilename } from "./FileUtils";
|
import { getIMGFilename } from "./FileUtils";
|
||||||
|
|
||||||
|
|
||||||
declare const PLUGIN_VERSION:string;
|
declare const PLUGIN_VERSION:string;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -384,19 +382,29 @@ export const scaleLoadedImage = (
|
|||||||
.filter((e: any) => e.type === "image" && e.fileId === f.id)
|
.filter((e: any) => e.type === "image" && e.fileId === f.id)
|
||||||
.forEach((el: any) => {
|
.forEach((el: any) => {
|
||||||
const [w_old, h_old] = [el.width, el.height];
|
const [w_old, h_old] = [el.width, el.height];
|
||||||
const elementAspectRatio = w_old / h_old;
|
if(f.shouldScale) {
|
||||||
if (imageAspectRatio != elementAspectRatio) {
|
const elementAspectRatio = w_old / h_old;
|
||||||
dirty = true;
|
if (imageAspectRatio != elementAspectRatio) {
|
||||||
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
dirty = true;
|
||||||
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
||||||
el.height = h_new;
|
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
||||||
el.width = w_new;
|
el.height = h_new;
|
||||||
el.y += (h_old - h_new) / 2;
|
el.width = w_new;
|
||||||
el.x += (w_old - w_new) / 2;
|
el.y += (h_old - h_new) / 2;
|
||||||
|
el.x += (w_old - w_new) / 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(w_old !== w_image || h_old !== h_image) {
|
||||||
|
dirty = true;
|
||||||
|
el.height = h_image;
|
||||||
|
el.width = w_image;
|
||||||
|
el.y += (h_old - h_image) / 2;
|
||||||
|
el.x += (w_old - w_image) / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { dirty, scene };
|
|
||||||
}
|
}
|
||||||
|
return { dirty, scene };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Document) => {
|
export const setDocLeftHandedMode = (isLeftHanded: boolean, ownerDocument:Document) => {
|
||||||
@@ -623,36 +631,15 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fragWithHTML = (html: string) =>
|
||||||
|
createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||||
|
|
||||||
export const errorlog = (data: {}) => {
|
export const errorlog = (data: {}) => {
|
||||||
console.error({ plugin: "Excalidraw", ...data });
|
console.error({ plugin: "Excalidraw", ...data });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
type GenericEvent = KeyboardEvent | MouseEvent | React.DragEvent | React.PointerEvent | React.MouseEvent;
|
|
||||||
|
|
||||||
export const isCtrlDown = (e:GenericEvent):boolean => {
|
|
||||||
https://stackoverflow.com/questions/44030187/correct-way-to-check-if-any-object-is-a-syntheticevent
|
|
||||||
return e instanceof Event
|
|
||||||
? e[CTRL_OR_CMD]
|
|
||||||
: e.nativeEvent instanceof DragEvent
|
|
||||||
? e[CTRL_OR_CMD] || isShiftDown(e) // React.DragEvent; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
|
||||||
: e[CTRL_OR_CMD];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isShiftDown = (e:GenericEvent):boolean => {
|
|
||||||
return e.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isAltDown = (e:GenericEvent):boolean => {
|
|
||||||
return e.altKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMetaDown = (e:GenericEvent):boolean => {
|
|
||||||
return e.metaKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**REACT 18
|
/**REACT 18
|
||||||
//see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/b67d70c5196f30e2968f9da919d106ee66f2a5eb
|
//see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/b67d70c5196f30e2968f9da919d106ee66f2a5eb
|
||||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/cc9d7828c7ee7755c1ef942519c43df32eae249f
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/commit/cc9d7828c7ee7755c1ef942519c43df32eae249f
|
||||||
|
|||||||
@@ -224,3 +224,7 @@ textarea.excalidraw-wysiwyg {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-tablet .excalidraw button {
|
||||||
|
padding: initial;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
|
|||||||
@@ -1676,6 +1676,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/chroma-js@^2.1.4":
|
||||||
|
"integrity" "sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ=="
|
||||||
|
"resolved" "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.4.tgz"
|
||||||
|
"version" "2.1.4"
|
||||||
|
|
||||||
"@types/codemirror@0.0.108":
|
"@types/codemirror@0.0.108":
|
||||||
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
|
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
|
||||||
"resolved" "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
"resolved" "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
||||||
@@ -2216,10 +2221,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@zerollup/ts-helpers" "^1.7.18"
|
"@zerollup/ts-helpers" "^1.7.18"
|
||||||
|
|
||||||
"@zsviczian/excalidraw@0.12.0-obsidian-9":
|
"@zsviczian/excalidraw@0.13.0-obsidian":
|
||||||
"integrity" "sha512-mJ1MB0eKgHjtXPxSCCQkn/z/hdg3pI9vQQuwyCqNs5hjspuLJ+DdGkhaHvX+HrDuFdwPl/s+vfPC/tjTj6tmbA=="
|
"integrity" "sha512-c4SnBEGKtenLB/1gSjXe3BVA+yZfo8b1p2E7sVcaPG8MTz6cpQsCB2+cv7Zta5ihIxuGfK3ZSepVhMbN7RFY2w=="
|
||||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.12.0-obsidian-9.tgz"
|
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.13.0-obsidian.tgz"
|
||||||
"version" "0.12.0-obsidian-9"
|
"version" "0.13.0-obsidian"
|
||||||
|
|
||||||
"abab@^2.0.3", "abab@^2.0.5":
|
"abab@^2.0.3", "abab@^2.0.5":
|
||||||
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||||
@@ -2964,6 +2969,11 @@
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"fsevents" "~2.3.2"
|
"fsevents" "~2.3.2"
|
||||||
|
|
||||||
|
"chroma-js@^2.4.2":
|
||||||
|
"integrity" "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
|
||||||
|
"resolved" "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz"
|
||||||
|
"version" "2.4.2"
|
||||||
|
|
||||||
"chrome-trace-event@^1.0.2":
|
"chrome-trace-event@^1.0.2":
|
||||||
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
||||||
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
|
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
|
||||||
@@ -4628,6 +4638,11 @@
|
|||||||
"call-bind" "^1.0.2"
|
"call-bind" "^1.0.2"
|
||||||
"get-intrinsic" "^1.1.1"
|
"get-intrinsic" "^1.1.1"
|
||||||
|
|
||||||
|
"gl-matrix@^3.4.3":
|
||||||
|
"integrity" "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||||
|
"resolved" "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz"
|
||||||
|
"version" "3.4.3"
|
||||||
|
|
||||||
"glob-parent@^5.1.2", "glob-parent@~5.1.2":
|
"glob-parent@^5.1.2", "glob-parent@~5.1.2":
|
||||||
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
|
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
|
||||||
"resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
"resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||||
@@ -6206,10 +6221,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"minimist" "^1.2.5"
|
"minimist" "^1.2.5"
|
||||||
|
|
||||||
"moment@2.29.3":
|
"moment@2.29.4":
|
||||||
"integrity" "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
|
"integrity" "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||||
"resolved" "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz"
|
"resolved" "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
|
||||||
"version" "2.29.3"
|
"version" "2.29.4"
|
||||||
|
|
||||||
"monkey-around@^2.3.0":
|
"monkey-around@^2.3.0":
|
||||||
"integrity" "sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA=="
|
"integrity" "sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA=="
|
||||||
@@ -6420,13 +6435,13 @@
|
|||||||
"define-properties" "^1.1.3"
|
"define-properties" "^1.1.3"
|
||||||
"es-abstract" "^1.19.1"
|
"es-abstract" "^1.19.1"
|
||||||
|
|
||||||
"obsidian@^0.15.4":
|
"obsidian@^0.16.3":
|
||||||
"integrity" "sha512-FE11CxxpVD6t/DBvjLvlT7q7YYW91ubTqPKIIp286LdnyLipS8Xi3Tif8i8ALPv87Vg9obKM43aWcPsYLxLllQ=="
|
"integrity" "sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw=="
|
||||||
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.15.4.tgz"
|
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.16.3.tgz"
|
||||||
"version" "0.15.4"
|
"version" "0.16.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/codemirror" "0.0.108"
|
"@types/codemirror" "0.0.108"
|
||||||
"moment" "2.29.3"
|
"moment" "2.29.4"
|
||||||
|
|
||||||
"obuf@^1.0.0", "obuf@^1.1.2":
|
"obuf@^1.0.0", "obuf@^1.1.2":
|
||||||
"integrity" "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
"integrity" "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||||
|
|||||||
Reference in New Issue
Block a user