From 88f256cd8f4c019b81be8d8ad2da6cebeacdc8d5 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 4 Jan 2025 20:23:14 +0100 Subject: [PATCH] Added comments to EA, moved non-class function to EAUtils --- src/core/main.ts | 3 +- src/core/managers/CommandManager.ts | 7 +- src/core/managers/MarkdownPostProcessor.ts | 2 +- src/shared/CropImage.ts | 3 +- src/shared/Dialogs/SelectCard.ts | 2 +- src/shared/EmbeddedFileLoader.ts | 2 +- src/shared/ExcalidrawAutomate.ts | 801 +++------------------ src/utils/AIUtils.ts | 2 +- src/utils/dynamicStyling.ts | 2 +- src/utils/excalidrawAutomateUtils.ts | 712 +++++++++++++++++- src/utils/getElementAtPointer.ts | 2 +- src/utils/utils.ts | 2 +- src/view/ExcalidrawView.ts | 4 +- src/view/components/menu/ToolsPanel.tsx | 2 +- 14 files changed, 828 insertions(+), 718 deletions(-) diff --git a/src/core/main.ts b/src/core/main.ts index 228ccab..2c8b16d 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -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 { diff --git a/src/core/managers/CommandManager.ts b/src/core/managers/CommandManager.ts index 7692017..c2c6367 100644 --- a/src/core/managers/CommandManager.ts +++ b/src/core/managers/CommandManager.ts @@ -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 { diff --git a/src/core/managers/MarkdownPostProcessor.ts b/src/core/managers/MarkdownPostProcessor.ts index 8ace685..edbc084 100644 --- a/src/core/managers/MarkdownPostProcessor.ts +++ b/src/core/managers/MarkdownPostProcessor.ts @@ -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"; diff --git a/src/shared/CropImage.ts b/src/shared/CropImage.ts index eb00371..df0ed9e 100644 --- a/src/shared/CropImage.ts +++ b/src/shared/CropImage.ts @@ -4,7 +4,8 @@ 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"; diff --git a/src/shared/Dialogs/SelectCard.ts b/src/shared/Dialogs/SelectCard.ts index 7530c01..aeb4d3b 100644 --- a/src/shared/Dialogs/SelectCard.ts +++ b/src/shared/Dialogs/SelectCard.ts @@ -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"; diff --git a/src/shared/EmbeddedFileLoader.ts b/src/shared/EmbeddedFileLoader.ts index c2fc540..b7b1523 100644 --- a/src/shared/EmbeddedFileLoader.ts +++ b/src/shared/EmbeddedFileLoader.ts @@ -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"; diff --git a/src/shared/ExcalidrawAutomate.ts b/src/shared/ExcalidrawAutomate.ts index e80df45..460598d 100644 --- a/src/shared/ExcalidrawAutomate.ts +++ b/src/shared/ExcalidrawAutomate.ts @@ -14,10 +14,10 @@ import { ExcalidrawTextContainer, } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { ColorMap, MimeType } from "./EmbeddedFileLoader"; -import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian"; +import { Editor, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian"; import * as obsidian_module from "obsidian"; -import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/view/ExcalidrawView"; -import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, getMarkdownDrawingSection, REGEX_LINK } from "./ExcalidrawData"; +import ExcalidrawView, { ExportSettings, TextMode } from "src/view/ExcalidrawView"; +import { ExcalidrawData, getMarkdownDrawingSection } from "./ExcalidrawData"; import { FRONTMATTER, nanoid, @@ -30,39 +30,24 @@ import { getLineHeight, getMaximumGroups, intersectElementWithLine, - measureText, DEVICE, - restore, - REG_LINKINDEX_INVALIDCHARS, - THEME_FILTER, mermaidToExcalidraw, refreshTextDimensions, - getFontFamilyString, - EXCALIDRAW_PLUGIN, } from "src/constants/constants"; -import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/fileUtils"; +import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/fileUtils"; import { //debug, - errorlog, - getEmbeddedFilenameParts, getImageSize, - getLinkParts, - getPNG, - getSVG, isMaskFile, - isVersionNewerThanOther, - scaleLoadedImage, wrapTextAtCharLength, arrayToMap, addAppendUpdateCustomData, } from "src/utils/utils"; import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/obsidianUtils"; import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; -import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader"; +import { EmbeddedFile, EmbeddedFilesLoader } from "./EmbeddedFileLoader"; import { tex2dataURL } from "./LaTeX"; -import { GenericInputPrompt, NewFileActions } from "src/shared/Dialogs/Prompt"; -import { t } from "src/lang/helpers"; -import { ScriptEngine } from "./Scripts"; +import { NewFileActions } from "src/shared/Dialogs/Prompt"; import { ConnectionPoint, DeviceType, Point } from "src/types/types"; import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster"; import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony"; @@ -92,12 +77,12 @@ import { extractCodeBlocks as _extractCodeBlocks, } from "../utils/AIUtils"; import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./Dialogs/SuggesterInfo"; -import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "../utils/excalidrawViewUtils"; +import { addBackOfTheNoteCard } from "../utils/excalidrawViewUtils"; import { log } from "../utils/debugHelper"; import { ExcalidrawLib } from "../types/excalidrawLib"; import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types"; import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes"; -import { errorMessage, filterColorMap, getEmbeddedFileForImageElment, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, svgColorInfoToColorMap, updateOrAddSVGColorInfo } from "src/utils/excalidrawAutomateUtils"; +import { _measureText, cloneElement, createPNG, createSVG, errorMessage, filterColorMap, getEmbeddedFileForImageElment, getFontFamily, getLineBox, getTemplate, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, normalizeLinePoints, repositionElementsToCursor, svgColorInfoToColorMap, updateOrAddSVGColorInfo, verifyMinimumPluginVersion } from "src/utils/excalidrawAutomateUtils"; extendPlugins([ HarmonyPlugin, @@ -121,6 +106,33 @@ declare const excalidrawLib: typeof ExcalidrawLib; const GAP = 4; +/** + * ExcalidrawAutomate is a utility class that provides a simplified API to interact with Excalidraw elements and the Excalidraw canvas. + * Elements in the Excalidraw Scene are immutable. You should never directly change element properties in the scene object. + * ExcalidrawAutomate provides a "workbench" where you can create, modify, and delete elements before committing them to the Excalidraw Scene. + * The basic workflow is to create elements in ExcalidrawAutomate and once ready commit them to the Excalidraw Scene using addElementsToView(). + * To modify elements in the scene, you should first copy them over to EA using copyViewElementsToEAforEditing, make the necessary modifications, + * then commit them back to the scene using addElementsToView(). + * To delete an element from the view set element.isDeleted = true and commit the changes to the scene using addElementsToView(). + * + * At a very high level, EA has 3 type of functions: + * - functions that modify elements in the EA workbench + * - functions that access elements and properties of the Scene + * - these only work if targetView is set using setView() + * - Scripts executed by the Excalidraw ScritpEngine will have the targetView set automatically + * - These functions include the word view in their name e.g. getViewSelectedElements() + * - utility functions that do not modify eleeemnts in the EA workbench or access the scene e.g. + * - ea.obsidian is a utility function that returns the Obsidian Module object. + * - eg.getCM() returns the ColorMaster object for manipulationg colors, + * - ea.help() provides information about functions and properties in the ExcalidrawAutomate class intended for use in Developer Console + * - checkAndCreateFolder (thought this has been superceeded by app.vault.createFolder in the Obsidian API) + * - etc. + * + * Note that some actions are asynchronous and require await to complete. e.g.: + * - addImage() + * - convertStringToDataURL() + * - etc. + */ export class ExcalidrawAutomate { /** * Utility function that returns the Obsidian Module object. @@ -129,14 +141,23 @@ export class ExcalidrawAutomate { return obsidian_module; }; + /** + * settings for the laser pointer + */ get LASERPOINTER() { return this.plugin.settings.laserSettings; } + /** + * Info about the device the script is running on. + */ get DEVICE():DeviceType { return DEVICE; } + /** + * Utility function to print detailed breakdown of startup time. + */ public printStartupBreakdown() { this.plugin.printStarupBreakdown(); } @@ -158,6 +179,9 @@ export class ExcalidrawAutomate { return addAppendUpdateCustomData(el,newData); } + /** + * Utility function to get help on ExcalidrawAutomate functions and properties intended to be used in Obsidian developer console. + */ public help(target: Function | string) { if (!target) { log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes"); @@ -294,6 +318,10 @@ export class ExcalidrawAutomate { return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin); } + /** + * Returns a new unique attachment filepath for the filename provided based on Obsidian settings + * @returns + */ public async getAttachmentFilepath(filename: string): Promise { if (!this.targetView || !this.targetView?.file) { errorMessage("targetView not set", "getAttachmentFolderAndFilePath()"); @@ -303,10 +331,20 @@ export class ExcalidrawAutomate { return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder); } + /** + * compresses a string to base64 using LZString + * @param str + * @returns + */ public compressToBase64(str:string): string { return LZString.compressToBase64(str); } + /** + * decompresses a string from base64 using LZString + * @param data + * @returns + */ public decompressFromBase64(data:string): string { if (!data) throw new Error("No input string provided for decompression."); let cleanedData = ''; @@ -397,6 +435,12 @@ export class ExcalidrawAutomate { return null; } + /** + * Checks if the Excalidraw File is a mask file. + * @param file + * @returns + */ + public isExcalidrawMaskFile(file?:TFile): boolean { if(file) { return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file); @@ -457,7 +501,7 @@ export class ExcalidrawAutomate { } /** - * + * Returns the Excalidraw API for the current view or the view provided. * @returns */ public getAPI(view?:ExcalidrawView):ExcalidrawAutomate { @@ -556,6 +600,7 @@ export class ExcalidrawAutomate { }; /** + * Generates a groupID and adds the groupId to all the elements in the objectIds array. Essentially grouping the elements in the view. * @param objectIds * @returns */ @@ -568,6 +613,7 @@ export class ExcalidrawAutomate { }; /** + * copies elements from ExcalidrawAutomate to the clipboard as a valid Excalidraw JSON string * @param templatePath */ async toClipboard(templatePath?: string) { @@ -591,6 +637,7 @@ export class ExcalidrawAutomate { }; /** + * Extracts the Excalidraw Scene from an Excalidraw File * @param file: TFile * @returns ExcalidrawScene */ @@ -846,7 +893,7 @@ export class ExcalidrawAutomate { }; /** - * + * Creates an SVG image from the ExcalidrawAutomate elements and the template provided. * @param templatePath * @param embedFont * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) @@ -903,7 +950,7 @@ export class ExcalidrawAutomate { /** - * + * Creates a PNG image from the ExcalidrawAutomate elements and the template provided. * @param templatePath * @param scale * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) @@ -959,7 +1006,7 @@ export class ExcalidrawAutomate { }; /** - * Wrapper for createPNG() that returns a base64 encoded string + * Wrapper for createPNG() that returns a base64 encoded string designed to support LLM workflows. * @param templatePath * @param scale * @param exportSettings @@ -990,6 +1037,18 @@ export class ExcalidrawAutomate { return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap); }; + /** + * Utility function. Returns an element object using style settings and provided parameters + * @param id + * @param eltype + * @param x + * @param y + * @param w + * @param h + * @param link + * @param scale + * @returns + */ private boxedElement( id: string, eltype: any, @@ -1033,12 +1092,23 @@ export class ExcalidrawAutomate { }; } - //retained for backward compatibility + /** + * Deprecated. Use addEmbeddable() instead. + * Retained for backward compatibility + * @param topX + * @param topY + * @param width + * @param height + * @param url + * @param file + * @returns + */ addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string { return this.addEmbeddable(topX, topY, width, height, url, file); } + /** - * + * Adds an embeddable element to the ExcalidrawAutomate instance. * @param topX * @param topY * @param width @@ -1054,7 +1124,6 @@ export class ExcalidrawAutomate { file?: TFile, embeddableCustomData?: EmbeddableMDCustomProps, ): string { - //@ts-ignore if (!this.targetView || !this.targetView?._loaded) { errorMessage("targetView not set", "addEmbeddable()"); return null; @@ -1953,8 +2022,8 @@ export class ExcalidrawAutomate { }; /** - * - * @param elToDelete + * Deletes elements in the view by removing them from the scene (ie. not by setting isDeleted to true) + * @param elToDelete: Array of Excalidraw Elements * @returns */ deleteViewElements(elToDelete: ExcalidrawElement[]): boolean { @@ -1967,10 +2036,11 @@ export class ExcalidrawAutomate { if (!api) { return false; } + const ids = elToDelete.map((e: ExcalidrawElement) => e.id); const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[]; const st: AppState = api.getAppState(); this.targetView.updateScene({ - elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)), + elements: el.filter((e: ExcalidrawElement) => !ids.includes(e.id)), appState: st, storeAction: "capture", }); @@ -3094,668 +3164,3 @@ export class ExcalidrawAutomate { this.colorPalette = {}; } }; - -export function initExcalidrawAutomate( - plugin: ExcalidrawPlugin, -): ExcalidrawAutomate { - const ea = new ExcalidrawAutomate(plugin); - //@ts-ignore - window.ExcalidrawAutomate = ea; - return ea; -} - -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; -} - -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]) - }; -} - -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 }; -} - -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; - - if(groupElements.length < scene.elements.length) { - fileIDWhiteList = new Set(); - 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 = `Placeholder`; - 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 { - 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 = 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 { - 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; -}; \ No newline at end of file diff --git a/src/utils/AIUtils.ts b/src/utils/AIUtils.ts index 4686320..943a6fe 100644 --- a/src/utils/AIUtils.ts +++ b/src/utils/AIUtils.ts @@ -1,4 +1,4 @@ -import { Notice, RequestUrlResponse, requestUrl } from "obsidian"; +import { Notice, RequestUrlResponse } from "obsidian"; import ExcalidrawPlugin from "src/core/main"; type MessageContent = diff --git a/src/utils/dynamicStyling.ts b/src/utils/dynamicStyling.ts index 9d536c8..635faac 100644 --- a/src/utils/dynamicStyling.ts +++ b/src/utils/dynamicStyling.ts @@ -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"; diff --git a/src/utils/excalidrawAutomateUtils.ts b/src/utils/excalidrawAutomateUtils.ts index 14e5368..5476995 100644 --- a/src/utils/excalidrawAutomateUtils.ts +++ b/src/utils/excalidrawAutomateUtils.ts @@ -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; + + if(groupElements.length < scene.elements.length) { + fileIDWhiteList = new Set(); + 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 = `Placeholder`; + 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 { + 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 = 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 { + 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; +}; \ No newline at end of file diff --git a/src/utils/getElementAtPointer.ts b/src/utils/getElementAtPointer.ts index 0c678f1..6d63d09 100644 --- a/src/utils/getElementAtPointer.ts +++ b/src/utils/getElementAtPointer.ts @@ -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, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8db3ed3..6339e56 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -25,7 +25,7 @@ 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"; diff --git a/src/view/ExcalidrawView.ts b/src/view/ExcalidrawView.ts index e241b3d..e51f08f 100644 --- a/src/view/ExcalidrawView.ts +++ b/src/view/ExcalidrawView.ts @@ -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, diff --git a/src/view/components/menu/ToolsPanel.tsx b/src/view/components/menu/ToolsPanel.tsx index 08238fa..483486c 100644 --- a/src/view/components/menu/ToolsPanel.tsx +++ b/src/view/components/menu/ToolsPanel.tsx @@ -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";