mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
3 Commits
2.7.5
...
2.7.5-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f256cd8f | ||
|
|
1562600cd3 | ||
|
|
90533138e5 |
@@ -39,7 +39,8 @@ import {
|
||||
setRootElementSize,
|
||||
} from "../constants/constants";
|
||||
import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings";
|
||||
import { initExcalidrawAutomate, ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import { initExcalidrawAutomate } from "src/utils/excalidrawAutomateUtils";
|
||||
import { around, dedupe } from "monkey-around";
|
||||
import { t } from "../lang/helpers";
|
||||
import {
|
||||
@@ -91,6 +92,15 @@ import { EventManager } from "./managers/EventManager";
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare const INITIAL_TIMESTAMP: number;
|
||||
|
||||
type FileMasterInfo = {
|
||||
isHyperLink: boolean;
|
||||
isLocalLink: boolean;
|
||||
path: string;
|
||||
hasSVGwithBitmap: boolean;
|
||||
blockrefData: string,
|
||||
colorMapJSON?: string
|
||||
}
|
||||
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
private fileManager: PluginFileManager;
|
||||
private observerManager: ObserverManager;
|
||||
@@ -113,7 +123,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
public opencount: number = 0;
|
||||
public ea: ExcalidrawAutomate;
|
||||
//A master list of fileIds to facilitate copy / paste
|
||||
public filesMaster: Map<FileId, { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
|
||||
public filesMaster: Map<FileId, FileMasterInfo> =
|
||||
null; //fileId, path
|
||||
public equationsMaster: Map<FileId, string> = null; //fileId, formula
|
||||
public mermaidsMaster: Map<FileId, string> = null; //fileId, mermaidText
|
||||
|
||||
@@ -29,11 +29,8 @@ import { InsertCommandDialog } from "../../shared/Dialogs/InsertCommandDialog";
|
||||
import { InsertImageDialog } from "../../shared/Dialogs/InsertImageDialog";
|
||||
import { ImportSVGDialog } from "../../shared/Dialogs/ImportSVGDialog";
|
||||
import { InsertMDDialog } from "../../shared/Dialogs/InsertMDDialog";
|
||||
import {
|
||||
ExcalidrawAutomate,
|
||||
insertLaTeXToView,
|
||||
search,
|
||||
} from "../../shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "../../shared/ExcalidrawAutomate";
|
||||
import { insertLaTeXToView, search } from "src/utils/excalidrawAutomateUtils";
|
||||
import { templatePromt } from "../../shared/Dialogs/Prompt";
|
||||
import { t } from "../../lang/helpers";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "obsidian";
|
||||
import { DEVICE, RERENDER_EVENT } from "../../constants/constants";
|
||||
import { EmbeddedFilesLoader } from "../../shared/EmbeddedFileLoader";
|
||||
import { createPNG, createSVG } from "../../shared/ExcalidrawAutomate";
|
||||
import { createPNG, createSVG } from "../../utils/excalidrawAutomateUtils";
|
||||
import { ExportSettings } from "../../view/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import {getIMGFilename,} from "../../utils/fileUtils";
|
||||
|
||||
@@ -4,11 +4,36 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
import { getEA } from "src/core";
|
||||
import { ExcalidrawAutomate, cloneElement } from "src/shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { cloneElement } from "src/utils/excalidrawAutomateUtils";
|
||||
import { ExportSettings } from "src/view/ExcalidrawView";
|
||||
import { nanoid } from "src/constants/constants";
|
||||
import { svgToBase64 } from "../utils/utils";
|
||||
|
||||
/**
|
||||
* Creates a masked image from an Excalidraw scene.
|
||||
*
|
||||
* The scene must contain:
|
||||
* - One element.type="frame" element that defines the crop area
|
||||
* - One or more element.type="image" elements
|
||||
* - Zero or more non-image shape elements (rectangles, ellipses etc) that define the mask
|
||||
*
|
||||
* The class splits the scene into two parts:
|
||||
* 1. Images (managed in imageEA)
|
||||
* 2. Mask shapes (managed in maskEA)
|
||||
*
|
||||
* A transparent rectangle matching the combined bounding box is added to both
|
||||
* imageEA and maskEA to ensure consistent sizing between image and mask.
|
||||
*
|
||||
* For performance, if there is only one image, it is not rotated, and
|
||||
* its size matches the bounding box,
|
||||
* the image data is used directly from cache rather than regenerating.
|
||||
*
|
||||
* @example
|
||||
* const cropper = new CropImage(elements, files);
|
||||
* const pngBlob = await cropper.getCroppedPNG();
|
||||
* cropper.destroy();
|
||||
*/
|
||||
export class CropImage {
|
||||
private imageEA: ExcalidrawAutomate;
|
||||
private maskEA: ExcalidrawAutomate;
|
||||
@@ -106,10 +131,15 @@ export class CropImage {
|
||||
withTheme: false,
|
||||
isMask: false,
|
||||
}
|
||||
const isRotated = this.imageEA.getElements().some(el=>el.type === "image" && el.angle !== 0);
|
||||
const images = Object.values(this.imageEA.imagesDict);
|
||||
if(!isRotated && (images.length === 1)) {
|
||||
return images[0].dataURL;
|
||||
const images = this.imageEA.getElements().filter(el=>el.type === "image" && el.isDeleted === false);
|
||||
const isRotated = images.some(el=>el.angle !== 0);
|
||||
const imageDataURLs = Object.values(this.imageEA.imagesDict);
|
||||
if(!isRotated && images.length === 1 && imageDataURLs.length === 1) {
|
||||
const { width, height } = this.bbox;
|
||||
if(images[0].width === width && images[0].height === height) {
|
||||
//get image from the cache if mask is not bigger than the image, and if there is a single image element
|
||||
return imageDataURLs[0].dataURL;
|
||||
}
|
||||
}
|
||||
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, FuzzySuggestModal, Notice, TFile } from "obsidian";
|
||||
import { App, FuzzySuggestModal, Notice } from "obsidian";
|
||||
import { t } from "../../lang/helpers";
|
||||
import ExcalidrawView from "src/view/ExcalidrawView";
|
||||
import { getEA } from "src/core";
|
||||
|
||||
@@ -157,6 +157,15 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addAppendUpdateCustomData",
|
||||
code: "addAppendUpdateCustomData(id: string, newData: Partial<Record<string, unknown>>)",
|
||||
desc: "Add, modify keys in element customData and preserve existing keys.\n" +
|
||||
"Creates customData={} if it does not exist.\n" +
|
||||
"Takes the element ID for an element in the elementsDict and the new data to add or modify.\n" +
|
||||
"To delete keys set key value in newData to undefined. so {keyToBeDeleted:undefined} will be deleted.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addToGroup",
|
||||
code: "addToGroup(objectIds: []): string;",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
FRONTMATTER_KEYS,
|
||||
getCSSFontDefinition,
|
||||
} from "../constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { createSVG } from "src/utils/excalidrawAutomateUtils";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
import { ExportSettings } from "../view/ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
@@ -73,7 +73,8 @@ type ImgData = {
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
size: Size;
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
};
|
||||
|
||||
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
|
||||
@@ -82,8 +83,16 @@ export type FileData = BinaryFileData & {
|
||||
size: Size;
|
||||
hasSVGwithBitmap: boolean;
|
||||
shouldScale: boolean; //true if image should maintain its area, false if image should display at 100% its size
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
};
|
||||
|
||||
export type PDFPageViewProps = {
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export type Size = {
|
||||
height: number;
|
||||
width: number;
|
||||
@@ -177,6 +186,7 @@ export class EmbeddedFile {
|
||||
public isLocalLink: boolean = false;
|
||||
public hyperlink:DataURL;
|
||||
public colorMap: ColorMap | null = null;
|
||||
public pdfPageViewProps: PDFPageViewProps;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
|
||||
this.plugin = plugin;
|
||||
@@ -252,12 +262,14 @@ export class EmbeddedFile {
|
||||
return this.mtime !== this.file.stat.mtime;
|
||||
}
|
||||
|
||||
public setImage(
|
||||
imgBase64: string,
|
||||
mimeType: MimeType,
|
||||
size: Size,
|
||||
isDark: boolean,
|
||||
isSVGwithBitmap: boolean,
|
||||
public setImage({ imgBase64, mimeType, size, isDark, isSVGwithBitmap, pdfPageViewProps } : {
|
||||
imgBase64: string;
|
||||
mimeType: MimeType;
|
||||
size: Size;
|
||||
isDark: boolean;
|
||||
isSVGwithBitmap: boolean;
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
}
|
||||
) {
|
||||
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
|
||||
return;
|
||||
@@ -266,6 +278,7 @@ export class EmbeddedFile {
|
||||
this.imgInverted = this.img = "";
|
||||
}
|
||||
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
|
||||
this.pdfPageViewProps = pdfPageViewProps;
|
||||
this.size = size;
|
||||
this.mimeType = mimeType;
|
||||
switch (isDark && isSVGwithBitmap) {
|
||||
@@ -345,6 +358,7 @@ export class EmbeddedFilesLoader {
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
}> {
|
||||
const result = await this._getObsidianImage(inFile, depth);
|
||||
this.emptyPDFDocsMap();
|
||||
@@ -552,9 +566,9 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
const excalidrawSVG = isExcalidrawFile ? dURL : null;
|
||||
|
||||
const [pdfDataURL, pdfSize] = isPDF
|
||||
const [pdfDataURL, pdfSize, pdfPageViewProps] = isPDF
|
||||
? await this.pdfToDataURL(file,linkParts)
|
||||
: [null, null];
|
||||
: [null, null, null];
|
||||
|
||||
let mimeType: MimeType = isPDF
|
||||
? "image/png"
|
||||
@@ -600,6 +614,7 @@ export class EmbeddedFilesLoader {
|
||||
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
|
||||
hasSVGwithBitmap,
|
||||
size,
|
||||
pdfPageViewProps,
|
||||
};
|
||||
} catch(e) {
|
||||
return null;
|
||||
@@ -634,7 +649,7 @@ export class EmbeddedFilesLoader {
|
||||
files.push([]);
|
||||
let batch = 0;
|
||||
|
||||
function* loadIterator():Generator<Promise<void>> {
|
||||
function* loadIterator(this: EmbeddedFilesLoader):Generator<Promise<void>> {
|
||||
while (!(entry = entries.next()).done) {
|
||||
if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue;
|
||||
const embeddedFile: EmbeddedFile = entry.value[1];
|
||||
@@ -654,20 +669,22 @@ export class EmbeddedFilesLoader {
|
||||
created: data.created,
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: data.hasSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
shouldScale: embeddedFile.shouldScale(),
|
||||
pdfPageViewProps: data.pdfPageViewProps,
|
||||
};
|
||||
files[batch].push(fileData);
|
||||
}
|
||||
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
|
||||
//this will reload the image in light/dark mode when switching themes
|
||||
const fileData = {
|
||||
const fileData: FileData = {
|
||||
mimeType: embeddedFile.mimeType,
|
||||
id: id,
|
||||
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
|
||||
created: embeddedFile.mtime,
|
||||
size: embeddedFile.size,
|
||||
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
shouldScale: embeddedFile.shouldScale(),
|
||||
pdfPageViewProps: embeddedFile.pdfPageViewProps,
|
||||
};
|
||||
files[batch].push(fileData);
|
||||
}
|
||||
@@ -803,7 +820,7 @@ export class EmbeddedFilesLoader {
|
||||
private async pdfToDataURL(
|
||||
file: TFile,
|
||||
linkParts: LinkParts,
|
||||
): Promise<[DataURL,{width:number, height:number}]> {
|
||||
): Promise<[DataURL,Size, PDFPageViewProps]> {
|
||||
try {
|
||||
let width = 0, height = 0;
|
||||
const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file);
|
||||
@@ -814,6 +831,7 @@ export class EmbeddedFilesLoader {
|
||||
const scale = this.plugin.settings.pdfScale;
|
||||
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
|
||||
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
|
||||
let viewProps: PDFPageViewProps;
|
||||
|
||||
// Render the page
|
||||
const renderPage = async (num:number) => {
|
||||
@@ -824,8 +842,8 @@ export class EmbeddedFilesLoader {
|
||||
const page = await pdfDoc.getPage(num);
|
||||
// Set scale
|
||||
const viewport = page.getViewport({ scale });
|
||||
height = canvas.height = viewport.height;
|
||||
width = canvas.width = viewport.width;
|
||||
height = canvas.height = Math.round(viewport.height);
|
||||
width = canvas.width = Math.round(viewport.width);
|
||||
|
||||
const renderCtx = {
|
||||
canvasContext: ctx,
|
||||
@@ -846,9 +864,10 @@ export class EmbeddedFilesLoader {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const [left, bottom, right, top] = page.view;
|
||||
viewProps = {left, bottom, right, top};
|
||||
|
||||
if(validRect) {
|
||||
const [left, bottom, _, top] = page.view;
|
||||
|
||||
const pageHeight = top - bottom;
|
||||
width = (cropRect[2] - cropRect[0]) * scale;
|
||||
height = (cropRect[3] - cropRect[1]) * scale;
|
||||
@@ -868,19 +887,19 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
const canvas = await renderPage(pageNum);
|
||||
if(canvas) {
|
||||
const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
|
||||
const result: [DataURL,Size, PDFPageViewProps] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async (blob) => {
|
||||
const dataURL = await blobToBase64(blob);
|
||||
resolve(dataURL);
|
||||
});
|
||||
})}` as DataURL, {width, height}];
|
||||
})}` as DataURL, {width, height}, viewProps];
|
||||
canvas.width = 0; //free memory iOS bug
|
||||
canvas.height = 0;
|
||||
return result;
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
return [null,null];
|
||||
return [null, null, null];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -649,7 +649,6 @@ export class ExcalidrawData {
|
||||
containers.forEach((container: any) => {
|
||||
if(ellipseAndRhombusContainerWrapping && !container.customData?.legacyTextWrap) {
|
||||
addAppendUpdateCustomData(container, {legacyTextWrap: true});
|
||||
//container.customData = {...container.customData, legacyTextWrap: true};
|
||||
}
|
||||
const filteredBoundElements = container.boundElements.filter(
|
||||
(boundEl: any) => elements.some((el: any) => el.id === boundEl.id),
|
||||
@@ -1569,13 +1568,13 @@ export class ExcalidrawData {
|
||||
filepath,
|
||||
);
|
||||
|
||||
embeddedFile.setImage(
|
||||
dataURL,
|
||||
embeddedFile.setImage({
|
||||
imgBase64: dataURL,
|
||||
mimeType,
|
||||
{ height: 0, width: 0 },
|
||||
scene.appState?.theme === "dark",
|
||||
mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
|
||||
);
|
||||
size: { height: 0, width: 0 },
|
||||
isDark: scene.appState?.theme === "dark",
|
||||
isSVGwithBitmap: mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
|
||||
});
|
||||
this.setFile(key as FileId, embeddedFile);
|
||||
return file;
|
||||
}
|
||||
@@ -1593,7 +1592,9 @@ export class ExcalidrawData {
|
||||
const pageRef = ef.linkParts.original.split("#")?.[1];
|
||||
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
|
||||
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
|
||||
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
|
||||
const link = ef.linkParts.original +
|
||||
getPDFRect({elCrop: el.crop, scale: pdfScale, customData: el.customData}) +
|
||||
(restOfLink ? restOfLink : "]]");
|
||||
el.link = `[[${link}`;
|
||||
this.elementLinks.set(el.id, el.link);
|
||||
dirty = true;
|
||||
@@ -1992,7 +1993,7 @@ export class ExcalidrawData {
|
||||
isLocalLink: data.isLocalLink,
|
||||
path: data.hyperlink,
|
||||
blockrefData: null,
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { TFile } from "obsidian";
|
||||
import { FileId } from "src/core";
|
||||
import { ColorMap, MimeType } from "src/shared/EmbeddedFileLoader";
|
||||
import { ColorMap, MimeType, PDFPageViewProps, Size } from "src/shared/EmbeddedFileLoader";
|
||||
|
||||
export type SVGColorInfo = Map<string, {
|
||||
mappedTo: string;
|
||||
@@ -19,8 +19,9 @@ export type ImageInfo = {
|
||||
file?:string | TFile,
|
||||
hasSVGwithBitmap: boolean,
|
||||
latex?: string,
|
||||
size?: {height: number, width: number},
|
||||
size?: Size,
|
||||
colorMap?: ColorMap,
|
||||
pdfPageViewProps?: PDFPageViewProps,
|
||||
}
|
||||
|
||||
export interface AddImageOptions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
|
||||
import { Notice, RequestUrlResponse } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
|
||||
type MessageContent =
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
//for future use, not used currently
|
||||
|
||||
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { PDFPageViewProps } from "src/shared/EmbeddedFileLoader";
|
||||
|
||||
export function getPDFCropRect (props: {
|
||||
scale: number,
|
||||
link: string,
|
||||
naturalHeight: number,
|
||||
naturalWidth: number,
|
||||
pdfPageViewProps: PDFPageViewProps,
|
||||
}) : ImageCrop | null {
|
||||
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
|
||||
if (!rectVal || rectVal.length !== 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { left, bottom } = props.pdfPageViewProps;
|
||||
const R0 = parseInt(rectVal[1]);
|
||||
const R1 = parseInt(rectVal[2]);
|
||||
const R2 = parseInt(rectVal[3]);
|
||||
const R3 = parseInt(rectVal[4]);
|
||||
|
||||
return {
|
||||
x: R0 * props.scale,
|
||||
y: (props.naturalHeight/props.scale - R3) * props.scale,
|
||||
x: (R0 - left) * props.scale,
|
||||
y: (bottom + props.naturalHeight/props.scale - R3) * props.scale,
|
||||
width: (R2 - R0) * props.scale,
|
||||
height: (R3 - R1) * props.scale,
|
||||
naturalWidth: props.naturalWidth,
|
||||
@@ -28,10 +31,16 @@ export function getPDFCropRect (props: {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
|
||||
const R0 = elCrop.x / scale;
|
||||
export function getPDFRect({elCrop, scale, customData}:{
|
||||
elCrop: ImageCrop, scale: number, customData: Record<string, unknown>
|
||||
}): string {
|
||||
const { left, bottom } = (customData && customData.pdfPageViewProps)
|
||||
? customData.pdfPageViewProps as PDFPageViewProps
|
||||
: { left: 0, bottom: 0 };
|
||||
|
||||
const R0 = elCrop.x / scale + left;
|
||||
const R2 = elCrop.width / scale + R0;
|
||||
const R3 = (elCrop.naturalHeight - elCrop.y) / scale;
|
||||
const R3 = bottom + (elCrop.naturalHeight - elCrop.y) / scale;
|
||||
const R1 = R3 - elCrop.height / scale;
|
||||
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { ColorMaster } from "@zsviczian/colormaster";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/view/ExcalidrawView";
|
||||
import { DynamicStyle } from "src/types/types";
|
||||
import { cloneElement } from "src/shared/ExcalidrawAutomate";
|
||||
import { cloneElement } from "./excalidrawAutomateUtils";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { addAppendUpdateCustomData } from "./utils";
|
||||
import { mutateElement } from "src/constants/constants";
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { errorlog } from "./utils";
|
||||
import { ColorMap } from "src/shared/EmbeddedFileLoader";
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { normalizePath, TFile } from "obsidian";
|
||||
|
||||
import ExcalidrawView, { ExportSettings, getTextMode } from "src/view/ExcalidrawView";
|
||||
import {
|
||||
GITHUB_RELEASES,
|
||||
getCommonBoundingBox,
|
||||
restore,
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
THEME_FILTER,
|
||||
EXCALIDRAW_PLUGIN,
|
||||
getFontFamilyString,
|
||||
getLineHeight,
|
||||
measureText,
|
||||
} from "src/constants/constants";
|
||||
import {
|
||||
//debug,
|
||||
errorlog,
|
||||
getEmbeddedFilenameParts,
|
||||
getLinkParts,
|
||||
getPNG,
|
||||
getSVG,
|
||||
isVersionNewerThanOther,
|
||||
scaleLoadedImage,
|
||||
} from "src/utils/utils";
|
||||
import { GenericInputPrompt, NewFileActions } from "src/shared/Dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import {
|
||||
postOpenAI as _postOpenAI,
|
||||
extractCodeBlocks as _extractCodeBlocks,
|
||||
} from "../utils/AIUtils";
|
||||
import { ColorMap, EmbeddedFilesLoader, FileData } from "src/shared/EmbeddedFileLoader";
|
||||
import { SVGColorInfo } from "src/types/excalidrawAutomateTypes";
|
||||
import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, REGEX_LINK } from "src/shared/ExcalidrawData";
|
||||
import { getFrameBasedOnFrameNameOrId } from "./excalidrawViewUtils";
|
||||
import { ScriptEngine } from "src/shared/Scripts";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export function isSVGColorInfo(obj: ColorMap | SVGColorInfo): boolean {
|
||||
return (
|
||||
@@ -112,3 +153,668 @@ export function isColorStringTransparent(color: string): boolean {
|
||||
|
||||
return rgbaHslaTransparentRegex.test(color) || hexTransparentRegex.test(color);
|
||||
}
|
||||
|
||||
export function initExcalidrawAutomate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
): ExcalidrawAutomate {
|
||||
const ea = new ExcalidrawAutomate(plugin);
|
||||
//@ts-ignore
|
||||
window.ExcalidrawAutomate = ea;
|
||||
return ea;
|
||||
}
|
||||
|
||||
export function normalizeLinePoints(
|
||||
points: [x: number, y: number][],
|
||||
//box: { x: number; y: number; w: number; h: number },
|
||||
): number[][] {
|
||||
const p = [];
|
||||
const [x, y] = points[0];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
p.push([points[i][0] - x, points[i][1] - y]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
export function getLineBox(
|
||||
points: [x: number, y: number][]
|
||||
):{x:number, y:number, w: number, h:number} {
|
||||
const [x1, y1, x2, y2] = estimateLineBound(points);
|
||||
return {
|
||||
x: x1,
|
||||
y: y1,
|
||||
w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]),
|
||||
h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1])
|
||||
};
|
||||
}
|
||||
|
||||
export function getFontFamily(id: number):string {
|
||||
return getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
newText: string,
|
||||
fontSize: number,
|
||||
fontFamily: number,
|
||||
lineHeight: number,
|
||||
): {w: number, h:number} {
|
||||
//following odd error with mindmap on iPad while synchornizing with desktop.
|
||||
if (!fontSize) {
|
||||
fontSize = 20;
|
||||
}
|
||||
if (!fontFamily) {
|
||||
fontFamily = 1;
|
||||
lineHeight = getLineHeight(fontFamily);
|
||||
}
|
||||
const metrics = measureText(
|
||||
newText,
|
||||
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
|
||||
lineHeight
|
||||
);
|
||||
return { w: metrics.width, h: metrics.height };
|
||||
}
|
||||
|
||||
export async function getTemplate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
fileWithPath: string,
|
||||
loadFiles: boolean = false,
|
||||
loader: EmbeddedFilesLoader,
|
||||
depth: number,
|
||||
convertMarkdownLinksToObsidianURLs: boolean = false,
|
||||
): Promise<{
|
||||
elements: any;
|
||||
appState: any;
|
||||
frontmatter: string;
|
||||
files: any;
|
||||
hasSVGwithBitmap: boolean;
|
||||
plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
|
||||
}> {
|
||||
const app = plugin.app;
|
||||
const vault = app.vault;
|
||||
const filenameParts = getEmbeddedFilenameParts(fileWithPath);
|
||||
const templatePath = normalizePath(filenameParts.filepath);
|
||||
const file = app.metadataCache.getFirstLinkpathDest(templatePath, "");
|
||||
let hasSVGwithBitmap = false;
|
||||
if (file && file instanceof TFile) {
|
||||
const data = (await vault.read(file))
|
||||
.replaceAll("\r\n", "\n")
|
||||
.replaceAll("\r", "\n");
|
||||
const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin);
|
||||
|
||||
if (file.extension === "excalidraw") {
|
||||
await excalidrawData.loadLegacyData(data, file);
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements: excalidrawData.scene.elements,
|
||||
hostFile: file,
|
||||
}) : excalidrawData.scene.elements,
|
||||
appState: excalidrawData.scene.appState,
|
||||
frontmatter: "",
|
||||
files: excalidrawData.scene.files,
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
const textMode = getTextMode(data);
|
||||
await excalidrawData.loadData(
|
||||
data,
|
||||
file,
|
||||
textMode,
|
||||
);
|
||||
|
||||
let trimLocation = data.search(/^##? Text Elements$/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search(/##? Drawing\n/);
|
||||
}
|
||||
|
||||
let scene = excalidrawData.scene;
|
||||
|
||||
let groupElements:ExcalidrawElement[] = scene.elements;
|
||||
if(filenameParts.hasGroupref) {
|
||||
const el = filenameParts.hasSectionref
|
||||
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
|
||||
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
|
||||
if(el.length > 0) {
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
|
||||
}
|
||||
}
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
el.type==="freedraw" ||
|
||||
( el.type==="image" &&
|
||||
!plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file)
|
||||
));
|
||||
}
|
||||
|
||||
let fileIDWhiteList:Set<FileId>;
|
||||
|
||||
if(groupElements.length < scene.elements.length) {
|
||||
fileIDWhiteList = new Set<FileId>();
|
||||
groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId));
|
||||
}
|
||||
|
||||
if (loadFiles) {
|
||||
//debug({where:"getTemplate",template:file.name,loader:loader.uid});
|
||||
await loader.loadSceneFiles({
|
||||
excalidrawData,
|
||||
addFiles: (fileArray: FileData[]) => {
|
||||
//, isDark: boolean) => {
|
||||
if (!fileArray || fileArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const f of fileArray) {
|
||||
if (f.hasSVGwithBitmap) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
excalidrawData.scene.files[f.id] = {
|
||||
mimeType: f.mimeType,
|
||||
id: f.id,
|
||||
dataURL: f.dataURL,
|
||||
created: f.created,
|
||||
};
|
||||
}
|
||||
scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
|
||||
},
|
||||
depth,
|
||||
fileIDWhiteList
|
||||
});
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
|
||||
let files:any = {};
|
||||
const sceneFilesSize = Object.values(scene.files).length;
|
||||
if (sceneFilesSize > 0) {
|
||||
if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) {
|
||||
Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => {
|
||||
files[f.id] = f;
|
||||
});
|
||||
} else {
|
||||
files = scene.files;
|
||||
}
|
||||
}
|
||||
|
||||
const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements: groupElements,
|
||||
hostFile: file,
|
||||
}) : groupElements,
|
||||
appState: scene.appState,
|
||||
frontmatter,
|
||||
plaintext: frontmatter !== filehead
|
||||
? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
|
||||
: "",
|
||||
files,
|
||||
hasSVGwithBitmap,
|
||||
};
|
||||
}
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
frontmatter: null,
|
||||
files: [],
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
|
||||
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#E7E7E7" /><text x="${width / 2}" y="${height / 2}" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="${Math.min(width, height) / 5}" fill="#888">Placeholder</text></svg>`;
|
||||
return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
|
||||
};
|
||||
|
||||
export async function createPNG(
|
||||
templatePath: string = undefined,
|
||||
scale: number = 1,
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
forceTheme: string = undefined,
|
||||
canvasTheme: string = undefined,
|
||||
canvasBackgroundColor: string = undefined,
|
||||
automateElements: ExcalidrawElement[] = [],
|
||||
plugin: ExcalidrawPlugin,
|
||||
depth: number,
|
||||
padding?: number,
|
||||
imagesDict?: any,
|
||||
): Promise<Blob> {
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
padding = padding ?? plugin.settings.exportPaddingSVG;
|
||||
const template = templatePath
|
||||
? await getTemplate(plugin, templatePath, true, loader, depth)
|
||||
: null;
|
||||
let elements = template?.elements ?? [];
|
||||
elements = elements.concat(automateElements);
|
||||
const files = imagesDict ?? {};
|
||||
if(template?.files) {
|
||||
Object.values(template.files).forEach((f:any)=>{
|
||||
if(!f.dataURL.startsWith("http")) {
|
||||
files[f.id]=f;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return await getPNG(
|
||||
{
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: GITHUB_RELEASES+PLUGIN_VERSION,
|
||||
elements,
|
||||
appState: {
|
||||
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
{
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
},
|
||||
padding,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
|
||||
export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
elements: ExcalidrawElement[];
|
||||
hostFile: TFile;
|
||||
}): ExcalidrawElement[] => {
|
||||
return elements.map((el)=>{
|
||||
if(el.link && el.link.startsWith("[")) {
|
||||
const partsArray = REGEX_LINK.getResList(el.link)[0];
|
||||
if(!partsArray?.value) return el;
|
||||
let linkText = REGEX_LINK.getLink(partsArray);
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, hostFile);
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
return el;
|
||||
}
|
||||
const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
hostFile.path,
|
||||
);
|
||||
if(!file) {
|
||||
return el;
|
||||
}
|
||||
let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
|
||||
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
|
||||
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
|
||||
originalLink: el.link,
|
||||
obsidianLink: link,
|
||||
linkedFile: file,
|
||||
hostFile: hostFile
|
||||
});
|
||||
}
|
||||
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
|
||||
newElement.link = link;
|
||||
return newElement;
|
||||
}
|
||||
return el;
|
||||
})
|
||||
}
|
||||
|
||||
function addFilterToForeignObjects(svg:SVGSVGElement):void {
|
||||
const foreignObjects = svg.querySelectorAll("foreignObject");
|
||||
foreignObjects.forEach((foreignObject) => {
|
||||
foreignObject.setAttribute("filter", THEME_FILTER);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSVG(
|
||||
templatePath: string = undefined,
|
||||
embedFont: boolean = false,
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
forceTheme: string = undefined,
|
||||
canvasTheme: string = undefined,
|
||||
canvasBackgroundColor: string = undefined,
|
||||
automateElements: ExcalidrawElement[] = [],
|
||||
plugin: ExcalidrawPlugin,
|
||||
depth: number,
|
||||
padding?: number,
|
||||
imagesDict?: any,
|
||||
convertMarkdownLinksToObsidianURLs: boolean = false,
|
||||
): Promise<SVGSVGElement> {
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
if(typeof exportSettings.skipInliningFonts === "undefined") {
|
||||
exportSettings.skipInliningFonts = !embedFont;
|
||||
}
|
||||
const template = templatePath
|
||||
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
|
||||
: null;
|
||||
let elements = template?.elements ?? [];
|
||||
elements = elements.concat(automateElements);
|
||||
padding = padding ?? plugin.settings.exportPaddingSVG;
|
||||
const files = imagesDict ?? {};
|
||||
if(template?.files) {
|
||||
Object.values(template.files).forEach((f:any)=>{
|
||||
files[f.id]=f;
|
||||
});
|
||||
}
|
||||
|
||||
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
|
||||
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
const svg = await getSVG(
|
||||
{
|
||||
//createAndOpenDrawing
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: GITHUB_RELEASES+PLUGIN_VERSION,
|
||||
elements,
|
||||
appState: {
|
||||
theme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
{
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
...filenameParts?.hasClippedFrameref
|
||||
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: {},
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
);
|
||||
|
||||
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
|
||||
|
||||
if(
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true)
|
||||
: elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
|
||||
if(el.length>0) {
|
||||
const containerId = el[0].containerId;
|
||||
if(containerId) {
|
||||
el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId));
|
||||
}
|
||||
const elBB = plugin.ea.getBoundingBox(el);
|
||||
const drawingBB = plugin.ea.getBoundingBox(elements);
|
||||
svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX;
|
||||
svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY;
|
||||
const width = elBB.width + 2*padding;
|
||||
svg.viewBox.baseVal.width = width;
|
||||
const height = elBB.height + 2*padding;
|
||||
svg.viewBox.baseVal.height = height;
|
||||
svg.setAttribute("width", `${width}`);
|
||||
svg.setAttribute("height", `${height}`);
|
||||
}
|
||||
}
|
||||
if (template?.hasSVGwithBitmap) {
|
||||
svg.setAttribute("hasbitmap", "true");
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
function estimateLineBound(points: any): [number, number, number, number] {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const [x, y] of points) {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
|
||||
return [minX, minY, maxX, maxY];
|
||||
}
|
||||
|
||||
export function estimateBounds(
|
||||
elements: ExcalidrawElement[],
|
||||
): [number, number, number, number] {
|
||||
const bb = getCommonBoundingBox(elements);
|
||||
return [bb.minX, bb.minY, bb.maxX, bb.maxY];
|
||||
}
|
||||
|
||||
export function repositionElementsToCursor(
|
||||
elements: ExcalidrawElement[],
|
||||
newPosition: { x: number; y: number },
|
||||
center: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
const [x1, y1, x2, y2] = estimateBounds(elements);
|
||||
let [offsetX, offsetY] = [0, 0];
|
||||
if (center) {
|
||||
[offsetX, offsetY] = [
|
||||
newPosition.x - (x1 + x2) / 2,
|
||||
newPosition.y - (y1 + y2) / 2,
|
||||
];
|
||||
} else {
|
||||
[offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1];
|
||||
}
|
||||
|
||||
elements.forEach((element: any) => {
|
||||
//using any so I can write read-only propery x & y
|
||||
element.x = element.x + offsetX;
|
||||
element.y = element.y + offsetY;
|
||||
});
|
||||
|
||||
return restore({elements}, null, null).elements;
|
||||
}
|
||||
|
||||
export const insertLaTeXToView = (view: ExcalidrawView) => {
|
||||
const app = view.plugin.app;
|
||||
const ea = view.plugin.ea;
|
||||
GenericInputPrompt.Prompt(
|
||||
view,
|
||||
view.plugin,
|
||||
app,
|
||||
t("ENTER_LATEX"),
|
||||
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
|
||||
view.plugin.settings.latexBoilerplate,
|
||||
undefined,
|
||||
3
|
||||
).then(async (formula: string) => {
|
||||
if (!formula) {
|
||||
return;
|
||||
}
|
||||
ea.reset();
|
||||
await ea.addLaTex(0, 0, formula);
|
||||
ea.setView(view);
|
||||
ea.addElementsToView(true, false, true);
|
||||
});
|
||||
};
|
||||
|
||||
export const search = async (view: ExcalidrawView) => {
|
||||
const ea = view.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(view);
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
let text = await ScriptEngine.inputPrompt(
|
||||
view,
|
||||
view.plugin,
|
||||
view.plugin.app,
|
||||
"Search for",
|
||||
"use quotation marks for exact match",
|
||||
"",
|
||||
);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const res = text.matchAll(/"(.*?)"/g);
|
||||
let query: string[] = [];
|
||||
let parts;
|
||||
while (!(parts = res.next()).done) {
|
||||
query.push(parts.value[1]);
|
||||
}
|
||||
text = text.replaceAll(/"(.*?)"/g, "");
|
||||
query = query.concat(text.split(" ").filter((s:string) => s.length !== 0));
|
||||
|
||||
ea.targetView.selectElementsMatchingQuery(elements, query);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getTextElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "text" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.rawText.toLowerCase().split("\n")[0].trim();
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getFrameElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "frame" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.name
|
||||
? el.name.toLowerCase().replaceAll("\n", " ").trim()
|
||||
: "";
|
||||
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getElementsWithLinkMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.link &&
|
||||
query.some((q) => {
|
||||
const text = el.link.toLowerCase().trim();
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getImagesMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
excalidrawData: ExcalidrawData,
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: ExcalidrawElement) =>
|
||||
el.type === "image" &&
|
||||
query.some((q) => {
|
||||
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
|
||||
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
|
||||
const text = filename ?? equation;
|
||||
if(!text) return false;
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
const newEl = JSON.parse(JSON.stringify(el));
|
||||
newEl.version = el.version + 1;
|
||||
newEl.updated = Date.now();
|
||||
newEl.versionNonce = Math.floor(Math.random() * 1000000000);
|
||||
return newEl;
|
||||
}
|
||||
|
||||
export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
|
||||
return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
|
||||
}
|
||||
|
||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||
return container?.boundElements?.length
|
||||
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
|
||||
: null;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../shared/ExcalidrawData";
|
||||
import ExcalidrawView, { TextMode } from "src/view/ExcalidrawView";
|
||||
import { rotatedDimensions } from "./utils";
|
||||
import { getBoundTextElementId } from "src/shared/ExcalidrawAutomate";
|
||||
import { getBoundTextElementId } from "src/utils/excalidrawAutomateUtils";
|
||||
|
||||
export const getElementsAtPointer = (
|
||||
pointer: any,
|
||||
|
||||
@@ -18,20 +18,21 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../view/ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./fileUtils";
|
||||
import { generateEmbeddableLink } from "./customEmbeddableUtils";
|
||||
import { FILENAMEPARTS } from "../types/utilTypes";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./obsidianUtils";
|
||||
import { updateElementLinksToObsidianLinks } from "src/shared/ExcalidrawAutomate";
|
||||
import { updateElementLinksToObsidianLinks } from "./excalidrawAutomateUtils";
|
||||
import { CropImage } from "../shared/CropImage";
|
||||
import opentype from 'opentype.js';
|
||||
import { runCompressionWorker } from "src/shared/Workers/compression-worker";
|
||||
import Pool from "es6-promise-pool";
|
||||
import { FileData } from "../shared/EmbeddedFileLoader";
|
||||
import { t } from "src/lang/helpers";
|
||||
import ExcalidrawScene from "src/shared/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -415,11 +416,17 @@ export async function getImageSize (
|
||||
});
|
||||
};
|
||||
|
||||
export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement {
|
||||
export function addAppendUpdateCustomData (
|
||||
el: Mutable<ExcalidrawElement>,
|
||||
newData: Partial<Record<string, unknown>>
|
||||
): ExcalidrawElement {
|
||||
if(!newData) return el;
|
||||
if(!el.customData) el.customData = {};
|
||||
for (const key in newData) {
|
||||
if(typeof newData[key] === "undefined") continue;
|
||||
if(typeof newData[key] === "undefined") {
|
||||
delete el.customData[key];
|
||||
continue;
|
||||
}
|
||||
el.customData[key] = newData[key];
|
||||
}
|
||||
return el;
|
||||
@@ -447,7 +454,7 @@ export function scaleLoadedImage (
|
||||
|
||||
scene.elements
|
||||
.filter((e: any) => e.type === "image" && e.fileId === img.id)
|
||||
.forEach((el: any) => {
|
||||
.forEach((el: Mutable<ExcalidrawImageElement>) => {
|
||||
const [elWidth, elHeight] = [el.width, el.height];
|
||||
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
|
||||
const elCrop: ImageCrop = el.crop;
|
||||
|
||||
@@ -57,16 +57,16 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import {
|
||||
repositionElementsToCursor,
|
||||
ExcalidrawAutomate,
|
||||
getTextElementsMatchingQuery,
|
||||
cloneElement,
|
||||
getFrameElementsMatchingQuery,
|
||||
getElementsWithLinkMatchingQuery,
|
||||
getImagesMatchingQuery,
|
||||
getBoundTextElementId
|
||||
} from "../shared/ExcalidrawAutomate";
|
||||
} from "../utils/excalidrawAutomateUtils";
|
||||
import { t } from "../lang/helpers";
|
||||
import {
|
||||
ExcalidrawData,
|
||||
@@ -105,8 +105,9 @@ import {
|
||||
shouldEmbedScene,
|
||||
_getContainerElement,
|
||||
arrayToMap,
|
||||
addAppendUpdateCustomData,
|
||||
} from "../utils/utils";
|
||||
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
|
||||
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
|
||||
import { splitFolderAndFilename } from "../utils/fileUtils";
|
||||
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "../shared/Dialogs/Prompt";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
@@ -147,6 +148,7 @@ import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
|
||||
import { getPDFCropRect } from "../utils/PDFUtils";
|
||||
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
|
||||
import { DropManager } from "./managers/DropManager";
|
||||
import { ImageInfo } from "src/types/excalidrawAutomateTypes";
|
||||
|
||||
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
|
||||
const PREVENT_RELOAD_TIMEOUT = 2000;
|
||||
@@ -223,7 +225,9 @@ export const addFiles = async (
|
||||
.filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
|
||||
.forEach((f:FileData) => {
|
||||
s.scene.elements
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && (
|
||||
(el.crop && el.crop.naturalWidth !== f.size.width) || !el.customData?.pdfPageViewProps
|
||||
))
|
||||
.forEach((el:Mutable<ExcalidrawImageElement>) => {
|
||||
s.dirty = true;
|
||||
const scale = f.size.width / el.crop.naturalWidth;
|
||||
@@ -235,6 +239,7 @@ export const addFiles = async (
|
||||
naturalWidth: f.size.width,
|
||||
naturalHeight: f.size.height,
|
||||
};
|
||||
addAppendUpdateCustomData(el, { pdfPageViewProps: f.pdfPageViewProps});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,13 +255,14 @@ export const addFiles = async (
|
||||
if (view.excalidrawData.hasFile(f.id)) {
|
||||
const embeddedFile = view.excalidrawData.getFile(f.id);
|
||||
|
||||
embeddedFile.setImage(
|
||||
f.dataURL,
|
||||
f.mimeType,
|
||||
f.size,
|
||||
embeddedFile.setImage({
|
||||
imgBase64: f.dataURL,
|
||||
mimeType: f.mimeType,
|
||||
size: f.size,
|
||||
isDark,
|
||||
f.hasSVGwithBitmap,
|
||||
);
|
||||
isSVGwithBitmap: f.hasSVGwithBitmap,
|
||||
pdfPageViewProps: f.pdfPageViewProps,
|
||||
});
|
||||
}
|
||||
if (view.excalidrawData.hasEquation(f.id)) {
|
||||
const latex = view.excalidrawData.getEquation(f.id).latex;
|
||||
@@ -1087,7 +1093,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
isFullscreen(): boolean {
|
||||
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen");
|
||||
return Boolean(document.body.querySelector(".excalidraw-hidden"));
|
||||
return Boolean(this.ownerDocument.body.querySelector(".excalidraw-hidden"));
|
||||
}
|
||||
|
||||
exitFullscreen() {
|
||||
@@ -3322,19 +3328,31 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.x<this.currentPosition.x || top.y>this.currentPosition.y || bottom.y<this.currentPosition.y;
|
||||
|
||||
const id = ea.addText(this.currentPosition.x, this.currentPosition.y, text);
|
||||
await this.addElements(ea.getElements(), isPointerOutsideVisibleArea, save, undefined, true);
|
||||
await this.addElements({
|
||||
newElements: ea.getElements(),
|
||||
repositionToCursor: isPointerOutsideVisibleArea,
|
||||
save: save,
|
||||
newElementsOnTop: true
|
||||
});
|
||||
ea.destroy();
|
||||
return id;
|
||||
};
|
||||
|
||||
public async addElements(
|
||||
newElements: ExcalidrawElement[],
|
||||
repositionToCursor: boolean = false,
|
||||
save: boolean = false,
|
||||
images: any,
|
||||
newElementsOnTop: boolean = false,
|
||||
shouldRestoreElements: boolean = false,
|
||||
): Promise<boolean> {
|
||||
public async addElements({
|
||||
newElements,
|
||||
repositionToCursor = false,
|
||||
save = false,
|
||||
images,
|
||||
newElementsOnTop = false,
|
||||
shouldRestoreElements = false,
|
||||
}: {
|
||||
newElements: ExcalidrawElement[];
|
||||
repositionToCursor?: boolean;
|
||||
save?: boolean;
|
||||
images?: {[key: FileId]: ImageInfo};
|
||||
newElementsOnTop?: boolean;
|
||||
shouldRestoreElements?: boolean;
|
||||
}): Promise<boolean> {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if (!api) {
|
||||
@@ -3391,40 +3409,38 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
? el.concat(newElements.filter((e) => !removeList.includes(e.id)))
|
||||
: newElements.filter((e) => !removeList.includes(e.id)).concat(el);
|
||||
|
||||
this.updateScene(
|
||||
{
|
||||
elements,
|
||||
storeAction: "capture",
|
||||
},
|
||||
shouldRestoreElements,
|
||||
);
|
||||
|
||||
if (images && Object.keys(images).length >0) {
|
||||
const files: BinaryFileData[] = [];
|
||||
Object.keys(images).forEach((k) => {
|
||||
const files: BinaryFileData[] = [];
|
||||
if (images && Object.keys(images).length >0) {
|
||||
Object.keys(images).forEach((k: FileId) => {
|
||||
files.push({
|
||||
mimeType: images[k].mimeType,
|
||||
id: images[k].id,
|
||||
dataURL: images[k].dataURL,
|
||||
created: images[k].created,
|
||||
});
|
||||
if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) {
|
||||
if (images[k].file || images[k].isHyperLink) { //|| images[k].isLocalLink but isLocalLink was never passed
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
images[k].isHyperLink && !images[k].isLocalLink
|
||||
images[k].isHyperLink //&& !images[k].isLocalLink local link is never passed to addElements
|
||||
? images[k].hyperlink
|
||||
: images[k].file,
|
||||
: (typeof images[k].file === "string" ? images[k].file : images[k].file.path),
|
||||
);
|
||||
const st: AppState = api.getAppState();
|
||||
embeddedFile.setImage(
|
||||
images[k].dataURL,
|
||||
images[k].mimeType,
|
||||
images[k].size,
|
||||
st.theme === "dark",
|
||||
images[k].hasSVGwithBitmap,
|
||||
);
|
||||
embeddedFile.setImage({
|
||||
imgBase64: images[k].dataURL,
|
||||
mimeType: images[k].mimeType,
|
||||
size: images[k].size,
|
||||
isDark: st.theme === "dark",
|
||||
isSVGwithBitmap: images[k].hasSVGwithBitmap,
|
||||
pdfPageViewProps: images[k].pdfPageViewProps,
|
||||
});
|
||||
this.excalidrawData.setFile(images[k].id, embeddedFile);
|
||||
if(images[k].pdfPageViewProps) {
|
||||
elements.filter((e) => e.type === "image" && e.fileId === images[k].id).forEach((e) => {
|
||||
addAppendUpdateCustomData(e, {pdfPageViewProps: images[k].pdfPageViewProps});
|
||||
});
|
||||
}
|
||||
}
|
||||
if (images[k].latex) {
|
||||
this.excalidrawData.setEquation(images[k].id, {
|
||||
@@ -3433,8 +3449,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.updateScene(
|
||||
{
|
||||
elements,
|
||||
storeAction: "capture",
|
||||
},
|
||||
shouldRestoreElements,
|
||||
);
|
||||
|
||||
if(files.length > 0) {
|
||||
api.addFiles(files);
|
||||
}
|
||||
|
||||
api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer));
|
||||
if (save) {
|
||||
await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
|
||||
@@ -3993,7 +4021,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
link,
|
||||
naturalHeight: fd.size.height,
|
||||
naturalWidth: fd.size.width,
|
||||
pdfPageViewProps: fd.pdfPageViewProps,
|
||||
});
|
||||
addAppendUpdateCustomData(el, {pdfPageViewProps: fd.pdfPageViewProps});
|
||||
if(el.crop) {
|
||||
el.width = el.crop.width/this.plugin.settings.pdfScale;
|
||||
el.height = el.crop.height/this.plugin.settings.pdfScale;
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS, saveIcon, stringToSVG } from "../../../constants/actionIcons";
|
||||
import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../../../constants/constants";
|
||||
import { insertLaTeXToView, search } from "../../../shared/ExcalidrawAutomate";
|
||||
import { insertLaTeXToView, search } from "../../../utils/excalidrawAutomateUtils";
|
||||
import ExcalidrawView, { TextMode } from "../../ExcalidrawView";
|
||||
import { t } from "../../../lang/helpers";
|
||||
import { ReleaseNotes } from "../../../shared/Dialogs/ReleaseNotes";
|
||||
|
||||
Reference in New Issue
Block a user