diff --git a/docs/API/attributes_functions_overview.md b/docs/API/attributes_functions_overview.md index 7be8045..4947190 100644 --- a/docs/API/attributes_functions_overview.md +++ b/docs/API/attributes_functions_overview.md @@ -5,152 +5,173 @@ Here's the interface implemented by ExcalidrawAutomate: ```typescript export interface ExcalidrawAutomate { plugin: ExcalidrawPlugin; - elementsDict: {}; - imagesDict: {}; + elementsDict: {}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id + imagesDict: {}; //the images files including DataURL, indexed by fileId style: { - strokeColor: string; + strokeColor: string; //https://www.w3schools.com/colors/default.asp backgroundColor: string; - angle: number; - fillStyle: FillStyle; + angle: number; //radian + fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid" strokeWidth: number; - storkeStyle: StrokeStyle; + storkeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted" roughness: number; opacity: number; - strokeSharpness: StrokeSharpness; - fontFamily: number; + strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp" + fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia fontSize: number; - textAlign: string; - verticalAlign: string; - startArrowHead: string; + textAlign: string; //"left"|"right"|"center" + verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently + startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null endArrowHead: string; - } - canvas: { - theme: string, - viewBackgroundColor: string, - gridSize: number }; - setFillStyle (val:number): void; - setStrokeStyle (val:number): void; - setStrokeSharpness (val:number): void; - setFontFamily (val:number): void; - setTheme (val:number): void; - addToGroup (objectIds:[]):string; - toClipboard (templatePath?:string): void; - getElements ():ExcalidrawElement[]; - getElement (id:string):ExcalidrawElement; - create ( - params?: { - filename?: string, - foldername?:string, - templatePath?:string, - onNewPane?: boolean, - frontmatterKeys?:{ - "excalidraw-plugin"?: "raw"|"parsed", - "excalidraw-link-prefix"?: string, - "excalidraw-link-brackets"?: boolean, - "excalidraw-url-prefix"?: string - } - } - ):Promise; - createSVG ( - templatePath?:string, - embedFont?:boolean, - exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean) - loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) - theme?:string - ):Promise; - createPNG ( - templatePath?:string, - scale?:number, - exportSettings?:ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean) - loader?:EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) - theme?:string - ):Promise; - wrapText (text:string, lineLen:number):string; - addRect (topX:number, topY:number, width:number, height:number):string; - addDiamond (topX:number, topY:number, width:number, height:number):string; - addEllipse (topX:number, topY:number, width:number, height:number):string; - addBlob (topX:number, topY:number, width:number, height:number):string; - addText ( - topX:number, - topY:number, - text:string, + canvas: { + theme: string; //"dark"|"light" + viewBackgroundColor: string; + gridSize: number; + }; + setFillStyle(val: number): void; //0:"hachure", 1:"cross-hatch" 2:"solid" + setStrokeStyle(val: number): void; //0:"solid", 1:"dashed", 2:"dotted" + setStrokeSharpness(val: number): void; //0:"round", 1:"sharp" + setFontFamily(val: number): void; //1: Virgil, 2:Helvetica, 3:Cascadia + setTheme(val: number): void; //0:"light", 1:"dark" + addToGroup(objectIds: []): string; + toClipboard(templatePath?: string): void; + getElements(): ExcalidrawElement[]; //get all elements from ExcalidrawAutomate elementsDict + getElement(id: string): ExcalidrawElement; //get single element from ExcalidrawAutomate elementsDict + create(params?: { //create a drawing and save it to filename + filename?: string; //if null: default filename as defined in Excalidraw settings + foldername?: string; //if null: default folder as defined in Excalidraw settings + templatePath?: string; + onNewPane?: boolean; + frontmatterKeys?: { + "excalidraw-plugin"?: "raw" | "parsed"; + "excalidraw-link-prefix"?: string; + "excalidraw-link-brackets"?: boolean; + "excalidraw-url-prefix"?: string; + }; + }): Promise; + createSVG( + templatePath?: string, + embedFont?: boolean, + exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean) + loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + theme?: string, + ): Promise; + createPNG( + templatePath?: string, + scale?: number, + exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean) + loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + theme?: string, + ): Promise; + wrapText(text: string, lineLen: number): string; + addRect(topX: number, topY: number, width: number, height: number): string; + addDiamond(topX: number, topY: number, width: number, height: number): string; + addEllipse(topX: number, topY: number, width: number, height: number): string; + addBlob(topX: number, topY: number, width: number, height: number): string; + addText( + topX: number, + topY: number, + text: string, formatting?: { - wrapAt?:number, - width?:number, - height?:number, - textAlign?: string, - box?: boolean|"box"|"blob"|"ellipse"|"diamond", - boxPadding?: number + wrapAt?: number; + width?: number; + height?: number; + textAlign?: string; + box?: boolean | "box" | "blob" | "ellipse" | "diamond"; //if !null, text will be boxed + boxPadding?: number; }, - id?:string - ):string; - addLine(points: [[x:number,y:number]]):string; - addArrow ( - points: [[x:number,y:number]], + id?: string, + ): string; + addLine(points: [[x: number, y: number]]): string; + addArrow( + points: [[x: number, y: number]], formatting?: { - startArrowHead?:string, - endArrowHead?:string, - startObjectId?:string, - endObjectId?:string - } - ):string ; - addImage(topX:number, topY:number, imageFile: TFile):Promise; - addLaTex(topX:number, topY:number, tex: string):Promise; - connectObjects ( - objectA: string, - connectionA: ConnectionPoint, - objectB: string, - connectionB: ConnectionPoint, + startArrowHead?: string; + endArrowHead?: string; + startObjectId?: string; + endObjectId?: string; + }, + ): string; + addImage(topX: number, topY: number, imageFile: TFile): Promise; + addLaTex(topX: number, topY: number, tex: string): Promise; + connectObjects( + objectA: string, + connectionA: ConnectionPoint, //type ConnectionPoint = "top" | "bottom" | "left" | "right" | null + objectB: string, + connectionB: ConnectionPoint, //when passed null, Excalidraw will automatically decide formatting?: { - numberOfPoints?: number, - startArrowHead?:string, - endArrowHead?:string, - padding?: number - } - ):void; - clear (): void; - reset (): void; - isExcalidrawFile (f:TFile): boolean; + numberOfPoints?: number; //points on the line. Default is 0 ie. line will only have a start and end point + startArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null + endArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null + padding?: number; + }, + ): void; + clear(): void; //clear elementsDict and imagesDict only + reset(): void; //clear() + reset all style values to default + isExcalidrawFile(f: TFile): boolean; //returns true if MD file is an Excalidraw file //view manipulation - targetView: ExcalidrawView; - setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView; - getExcalidrawAPI ():any; - getViewElements ():ExcalidrawElement[]; - deleteViewElements (el: ExcalidrawElement[]):boolean; - getViewSelectedElement ():ExcalidrawElement; - getViewSelectedElements ():ExcalidrawElement[]; - viewToggleFullScreen (forceViewMode?:boolean):void; - connectObjectWithViewSelectedElement ( - objectA:string, + targetView: ExcalidrawView; //the view currently edited + setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView; + getExcalidrawAPI(): any; //https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref + getViewElements(): ExcalidrawElement[]; //get elements in View + deleteViewElements(el: ExcalidrawElement[]): boolean; + getViewSelectedElement(): ExcalidrawElement; //get the selected element in the view, if more are selected, get the first + getViewSelectedElements(): ExcalidrawElement[]; + copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void; //copies elements from view to elementsDict for editing + viewToggleFullScreen(forceViewMode?: boolean): void; + connectObjectWithViewSelectedElement( //connect an object to the selected element in the view + objectA: string, //see connectObjects connectionA: ConnectionPoint, - connectionB: ConnectionPoint, + connectionB: ConnectionPoint, formatting?: { - numberOfPoints?: number, - startArrowHead?:string, - endArrowHead?:string, - padding?: number - } - ):boolean; - addElementsToView (repositionToCursor:boolean, save:boolean):Promise; - onDropHook (data: { - ea: ExcalidrawAutomate, - event: React.DragEvent, - draggable: any, //Obsidian draggable object - type: "file"|"text"|"unknown", - payload: { - files: TFile[], //TFile[] array of dropped files - text: string, //string + numberOfPoints?: number; + startArrowHead?: string; + endArrowHead?: string; + padding?: number; }, - excalidrawFile: TFile, //the file receiving the drop event - view: ExcalidrawView, //the excalidraw view receiving the drop - pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop - }):boolean; - mostRecentMarkdownSVG:SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes - //utility functions to generate EmbeddedFilesLoaderand ExportSettings objects - getEmbeddedFilesLoader(isDark?:boolean):EmbeddedFilesLoader; - getExportSettings(withBackground:boolean,withTheme:boolean):ExportSettings; - getBoundingBox(elements:ExcalidrawElement[]): {topX:number,topY:number,width:number,height:number}; + ): boolean; + addElementsToView( //Adds elements from elementsDict to the current view + repositionToCursor: boolean, + save: boolean, + ): Promise; + onDropHook(data: { //if set Excalidraw will call this function onDrop events + ea: ExcalidrawAutomate; + event: React.DragEvent; + draggable: any; //Obsidian draggable object + type: "file" | "text" | "unknown"; + payload: { + files: TFile[]; //TFile[] array of dropped files + text: string; //string + }; + excalidrawFile: TFile; //the file receiving the drop event + view: ExcalidrawView; //the excalidraw view receiving the drop + pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop + }): boolean; //a return of true will stop the default onDrop processing in Excalidraw + mostRecentMarkdownSVG: SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes + getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader; //utility function to generate EmbeddedFilesLoader object + getExportSettings( //utility function to generate ExportSettings object + withBackground: boolean, + withTheme: boolean, + ): ExportSettings; + getBoundingBox(elements: ExcalidrawElement[]): { //get bounding box of elements + topX: number; //bounding box is the box encapsulating all of the elements completely + topY: number; + width: number; + height: number; + }; + //elements grouped by the highest level groups + getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][]; + //gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box + getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement; + // Returns 2 or 0 intersection points between line going through `a` and `b` + // and the `element`, in ascending order of distance from `a`. + intersectElementWithLine( + element: ExcalidrawBindableElement, + a: readonly [number, number], + b: readonly [number, number], + gap?: number, //if given, element is inflated by this value + ): Point[]; } ``` diff --git a/docs/API/introduction.md b/docs/API/introduction.md index b0df236..aa2d3bb 100644 --- a/docs/API/introduction.md +++ b/docs/API/introduction.md @@ -1,6 +1,6 @@ # [◀ Excalidraw Automate How To](../readme.md) ## Introduction to the API -You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code: +You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting Templater, DataView and QuickAdd scripts with the following code: *Use CTRL+Shift+V to paste code into Obsidian!* ```javascript @@ -8,7 +8,9 @@ const ea = ExcalidrawAutomate; ea.reset(); ``` -The first line creates a practical constant so you can avoid writing ExcalidrawAutomate 100x times. +In case you are using the Excalidraw plugin's built in Scripting engine, the engine will take care of initializing the ea object. See [Excalidraw Script Engine](../ExcalidrawScriptsEngine.md) for more information. + +The first line creates a constant so you can avoid writing ExcalidrawAutomate 100x times. The second line resets ExcalidrawAutomate to defaults. This is important as you will not know which template you executed before, thus you won't know what state you left Excalidraw in. diff --git a/docs/ExcalidrawScriptsEngine.md b/docs/ExcalidrawScriptsEngine.md new file mode 100644 index 0000000..48be1ee --- /dev/null +++ b/docs/ExcalidrawScriptsEngine.md @@ -0,0 +1,48 @@ +# [◀ Excalidraw Automate How To](../readme.md) + +Place your ExcalidrawAutomate Scripts into the folder defined in Excalidraw Settings. EA scripts may be markdown files, but must contain valid JavaScript code. You will be able to access your scripts from Excalidraw via the Obsidian Command Palette. This will allow you to assign hotkeys to your favorite scripts just like to any other Obsidian command. The Scripts folder may not be the root folder of your Vault. + +An Excalidraw script will automatically receive two objects: +- The `ea` object, already initialized and set to the active view from which it was called. +- The `utils` object, which currently supports a single function: `inputPrompt: (header: string, placeholder?: string, value?: string)`. + +## Example Excalidraw Automate script + +### Add box around selected elements +This script will add an encapsulating box around the currently selected elements in Excalidraw +```javascript +padding = parseInt (await utils.inputPrompt("padding?")); +elements = ea.getViewSelectedElements(); +const box = ea.getBoundingBox(elements); +const rndColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0"); +ea.style.strokeColor = rndColor; +id = ea.addRect( + box.topX - padding, + box.topY - padding, + box.width + 2*padding, + box.height + 2*padding +); +ea.copyViewElementsToEAforEditing(elements); +ea.addToGroup([id].concat(elements.map((el)=>el.id))); +ea.addElementsToView(false); +``` + +### Connect selected elements with an arrow +```javascript +const elements = ea.getViewSelectedElements(); +ea.copyViewElementsToEAforEditing(elements); +const groups = ea.getMaximumGroups(elements); +if(groups.length !== 2) return; +els = [ + ea.getLargestElement(groups[0]), + ea.getLargestElement(groups[1]) +]; +ea.connectObjects( + els[0].id, + null, + els[1].id, + null, + {numberOfPoints:2} +); +ea.addElementsToView(); +``` \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md index ffeba04..5b60d22 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,18 +1,20 @@ # Excalidraw Automate How To -Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://silentvoid13.github.io/Templater/docs/) plugin, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) +Use ExcalidrawAutomate to create or manipulate Excalidraw drawings using Excalidraw Scripts (see settings), the [Templater](https://silentvoid13.github.io/Templater/docs/) or the [QuickAdd](https://github.com/chhoumann/quickadd) plugins, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) -With a little work, using Excalidraw Automate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, etc. based on documents in your vault. +With a little work, using ExcalidrawAutomate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, or automate simple tasks (i.e. create macros) in Excalidraw. ![image](https://user-images.githubusercontent.com/14358394/117549619-bae41180-b03b-11eb-968d-c909e79a7524.png) ## API documentation -- [Introduction to the API](API/introduction.md) +- **start here** [Introduction to the API](API/introduction.md) - [Overview of Attributes and Functions](API/attributes_functions_overview.md) - [Element Sytle](API/element_style.md) - [Canvas Style](API/canvas_style.md) - [Adding Objects](API/objects.md) - [Utility Functions](API/utility.md) +## Excalidraw Script Engine +Besides Templater, QuickAdd and DataView, you can also create ExcalidrawAutomate "macros" using the Scripts Engine. See more detailed description [here](ExcalidrawScriptsEngine.md). ## Examples - **Templater** diff --git a/esbuild.config.json b/esbuild.config.json index 6c29be6..8137c33 100644 --- a/esbuild.config.json +++ b/esbuild.config.json @@ -1,3 +1,4 @@ { - "minify": true + "minifyWhitespace": true, + "minifySyntax":true } \ No newline at end of file diff --git a/package.json b/package.json index 0d230c5..384c80e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "author": "", "license": "MIT", "dependencies": { - "@zsviczian/excalidraw": "0.10.0-obsidian-14", + "@zsviczian/excalidraw": "0.10.0-obsidian-18", "monkey-around": "^2.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index abed86e..5280947 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -4,6 +4,7 @@ import { StrokeStyle, StrokeSharpness, ExcalidrawElement, + ExcalidrawBindableElement, } from "@zsviczian/excalidraw/types/element/types"; import { normalizePath, TFile } from "obsidian"; import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView"; @@ -24,51 +25,58 @@ import { scaleLoadedImage, wrapText, } from "./Utils"; -import { AppState } from "@zsviczian/excalidraw/types/types"; +import { AppState, Point } from "@zsviczian/excalidraw/types/types"; import { EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader"; import { tex2dataURL } from "./LaTeX"; -import { getCommonBoundingBox } from "@zsviczian/excalidraw"; +import { + determineFocusDistance, + getCommonBoundingBox, + getMaximumGroups, + intersectElementWithLine, +} from "@zsviczian/excalidraw"; +import { start } from "repl"; -declare type ConnectionPoint = "top" | "bottom" | "left" | "right"; +declare type ConnectionPoint = "top" | "bottom" | "left" | "right" | null; +const GAP = 4; export interface ExcalidrawAutomate { plugin: ExcalidrawPlugin; - elementsDict: {}; - imagesDict: {}; + elementsDict: {}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id + imagesDict: {}; //the images files including DataURL, indexed by fileId style: { - strokeColor: string; + strokeColor: string; //https://www.w3schools.com/colors/default.asp backgroundColor: string; - angle: number; - fillStyle: FillStyle; + angle: number; //radian + fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid" strokeWidth: number; - storkeStyle: StrokeStyle; + storkeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted" roughness: number; opacity: number; - strokeSharpness: StrokeSharpness; - fontFamily: number; + strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp" + fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia fontSize: number; - textAlign: string; - verticalAlign: string; - startArrowHead: string; + textAlign: string; //"left"|"right"|"center" + verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently + startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null endArrowHead: string; }; canvas: { - theme: string; + theme: string; //"dark"|"light" viewBackgroundColor: string; gridSize: number; }; - setFillStyle(val: number): void; - setStrokeStyle(val: number): void; - setStrokeSharpness(val: number): void; - setFontFamily(val: number): void; - setTheme(val: number): void; + setFillStyle(val: number): void; //0:"hachure", 1:"cross-hatch" 2:"solid" + setStrokeStyle(val: number): void; //0:"solid", 1:"dashed", 2:"dotted" + setStrokeSharpness(val: number): void; //0:"round", 1:"sharp" + setFontFamily(val: number): void; //1: Virgil, 2:Helvetica, 3:Cascadia + setTheme(val: number): void; //0:"light", 1:"dark" addToGroup(objectIds: []): string; toClipboard(templatePath?: string): void; - getElements(): ExcalidrawElement[]; - getElement(id: string): ExcalidrawElement; - create(params?: { - filename?: string; - foldername?: string; + getElements(): ExcalidrawElement[]; //get all elements from ExcalidrawAutomate elementsDict + getElement(id: string): ExcalidrawElement; //get single element from ExcalidrawAutomate elementsDict + create(params?: { //create a drawing and save it to filename + filename?: string; //if null: default filename as defined in Excalidraw settings + foldername?: string; //if null: default folder as defined in Excalidraw settings templatePath?: string; onNewPane?: boolean; frontmatterKeys?: { @@ -81,15 +89,15 @@ export interface ExcalidrawAutomate { createSVG( templatePath?: string, embedFont?: boolean, - exportSettings?: ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean) - loader?: EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean) + loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) theme?: string, ): Promise; createPNG( templatePath?: string, scale?: number, - exportSettings?: ExportSettings, //see ExcalidrawAutomate.getExportSettings(boolean,boolean) - loader?: EmbeddedFilesLoader, //see ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean) + loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) theme?: string, ): Promise; wrapText(text: string, lineLen: number): string; @@ -106,7 +114,7 @@ export interface ExcalidrawAutomate { width?: number; height?: number; textAlign?: string; - box?: boolean | "box" | "blob" | "ellipse" | "diamond"; + box?: boolean | "box" | "blob" | "ellipse" | "diamond"; //if !null, text will be boxed boxPadding?: number; }, id?: string, @@ -125,32 +133,32 @@ export interface ExcalidrawAutomate { addLaTex(topX: number, topY: number, tex: string): Promise; connectObjects( objectA: string, - connectionA: ConnectionPoint, + connectionA: ConnectionPoint, //type ConnectionPoint = "top" | "bottom" | "left" | "right" | null objectB: string, - connectionB: ConnectionPoint, + connectionB: ConnectionPoint, //when passed null, Excalidraw will automatically decide formatting?: { - numberOfPoints?: number; - startArrowHead?: string; - endArrowHead?: string; + numberOfPoints?: number; //points on the line. Default is 0 ie. line will only have a start and end point + startArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null + endArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null padding?: number; }, ): void; - clear(): void; - reset(): void; - isExcalidrawFile(f: TFile): boolean; + clear(): void; //clear elementsDict and imagesDict only + reset(): void; //clear() + reset all style values to default + isExcalidrawFile(f: TFile): boolean; //returns true if MD file is an Excalidraw file //view manipulation - targetView: ExcalidrawView; - setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView; - getExcalidrawAPI(): any; - getViewElements(): ExcalidrawElement[]; + targetView: ExcalidrawView; //the view currently edited + setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView; + getExcalidrawAPI(): any; //https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref + getViewElements(): ExcalidrawElement[]; //get elements in View deleteViewElements(el: ExcalidrawElement[]): boolean; - getViewSelectedElement(): ExcalidrawElement; + getViewSelectedElement(): ExcalidrawElement; //get the selected element in the view, if more are selected, get the first getViewSelectedElements(): ExcalidrawElement[]; - copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void; //copies elements to elementsDict + copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void; //copies elements from view to elementsDict for editing viewToggleFullScreen(forceViewMode?: boolean): void; - connectObjectWithViewSelectedElement( - objectA: string, - connectionA: ConnectionPoint, + connectObjectWithViewSelectedElement( //connect an object to the selected element in the view + objectA: string, //see connectObjects + connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?: { numberOfPoints?: number; @@ -159,11 +167,11 @@ export interface ExcalidrawAutomate { padding?: number; }, ): boolean; - addElementsToView( + addElementsToView( //Adds elements from elementsDict to the current view repositionToCursor: boolean, save: boolean, ): Promise; - onDropHook(data: { + onDropHook(data: { //if set Excalidraw will call this function onDrop events ea: ExcalidrawAutomate; event: React.DragEvent; draggable: any; //Obsidian draggable object @@ -175,20 +183,31 @@ export interface ExcalidrawAutomate { excalidrawFile: TFile; //the file receiving the drop event view: ExcalidrawView; //the excalidraw view receiving the drop pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop - }): boolean; + }): boolean; //a return of true will stop the default onDrop processing in Excalidraw mostRecentMarkdownSVG: SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes - //utility functions to generate EmbeddedFilesLoaderand ExportSettings objects - getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader; - getExportSettings( + getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader; //utility function to generate EmbeddedFilesLoader object + getExportSettings( //utility function to generate ExportSettings object withBackground: boolean, withTheme: boolean, ): ExportSettings; - getBoundingBox(elements: ExcalidrawElement[]): { - topX: number; + getBoundingBox(elements: ExcalidrawElement[]): { //get bounding box of elements + topX: number; //bounding box is the box encapsulating all of the elements completely topY: number; width: number; height: number; }; + //elements grouped by the highest level groups + getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][]; + //gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box + getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement; + // Returns 2 or 0 intersection points between line going through `a` and `b` + // and the `element`, in ascending order of distance from `a`. + intersectElementWithLine( + element: ExcalidrawBindableElement, + a: readonly [number, number], + b: readonly [number, number], + gap?: number, //if given, element is inflated by this value + ): Point[]; } declare let window: any; @@ -682,13 +701,13 @@ export async function initExcalidrawAutomate( const id = nanoid(); //this.elementIds.push(id); this.elementsDict[id] = { - points: normalizeLinePoints(points, box), + points: normalizeLinePoints(points), lastCommittedPoint: null, startBinding: null, endBinding: null, startArrowhead: null, endArrowhead: null, - ...boxedElement(id, "line", box.x, box.y, box.w, box.h), + ...boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h), }; return id; }, @@ -703,23 +722,29 @@ export async function initExcalidrawAutomate( ): string { const box = getLineBox(points); const id = nanoid(); + const startPoint = points[0]; + const endPoint = points[points.length-1]; //this.elementIds.push(id); this.elementsDict[id] = { - points: normalizeLinePoints(points, box), + points: normalizeLinePoints(points), lastCommittedPoint: null, startBinding: { elementId: formatting?.startObjectId, - focus: 0.1, - gap: 4, + focus: formatting?.startObjectId ? determineFocusDistance(this.getElement(formatting?.startObjectId),endPoint,startPoint):0.1, + gap: GAP, + }, + endBinding: { + elementId: formatting?.endObjectId, + focus: formatting?.endObjectId ? determineFocusDistance(this.getElement(formatting?.endObjectId),startPoint,endPoint):0.1, + gap: GAP, }, - endBinding: { elementId: formatting?.endObjectId, focus: 0.1, gap: 4 }, startArrowhead: formatting?.startArrowHead ? formatting.startArrowHead : this.style.startArrowHead, endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead, - ...boxedElement(id, "arrow", box.x, box.y, box.w, box.h), + ...boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h), }; if (formatting?.startObjectId) { if (!this.elementsDict[formatting.startObjectId].boundElementIds) { @@ -845,8 +870,51 @@ export async function initExcalidrawAutomate( return [(el.x + (el.x + el.width)) / 2, el.y - padding]; } }; - const [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]); - const [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]); + let aX; + let aY; + let bX; + let bY; + const elA = this.elementsDict[objectA]; + const elB = this.elementsDict[objectB]; + if (!connectionA || !connectionB) { + const aCenterX = elA.x + elA.width / 2; + const bCenterX = elB.x + elB.width / 2; + const aCenterY = elA.y + elA.height / 2; + const bCenterY = elB.y + elB.height / 2; + if (!connectionA) { + const intersect = intersectElementWithLine( + elA, + [bCenterX, bCenterY], + [aCenterX, aCenterY], + GAP, + ); + if (intersect.length === 0) { + [aX, aY] = [aCenterX, aCenterY]; + } else { + [aX, aY] = intersect[0]; + } + } + + if (!connectionB) { + const intersect = intersectElementWithLine( + elB, + [aCenterX, aCenterY], + [bCenterX, bCenterY], + GAP, + ); + if (intersect.length === 0) { + [bX, bY] = [bCenterX, bCenterY]; + } else { + [bX, bY] = intersect[0]; + } + } + } + if (connectionA) { + [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]); + } + if (connectionB) { + [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]); + } const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end const points = []; for (let i = 0; i < numAP; i++) { @@ -972,10 +1040,10 @@ export async function initExcalidrawAutomate( .filter((e: any) => selectedElementsKeys.includes(e.id)); }, copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void { - elements.forEach((el)=>{ - this.elementsDict[el.id]={ - version: el.version+1, - ...el + elements.forEach((el) => { + this.elementsDict[el.id] = { + version: el.version + 1, + ...el, }; }); }, @@ -1031,7 +1099,7 @@ export async function initExcalidrawAutomate( }, async addElementsToView( repositionToCursor: boolean = false, - save: boolean = false, + save: boolean = true, ): Promise { if (!this.targetView || !this.targetView?._loaded) { errorMessage("targetView not set", "addElementsToView()"); @@ -1070,6 +1138,35 @@ export async function initExcalidrawAutomate( height: bb.maxY - bb.minY, }; }, + getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] { + return getMaximumGroups(elements); + }, + getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement { + if (!elements || elements.length === 0) { + return null; + } + let largestElement = elements[0]; + const getSize = (el: ExcalidrawElement): Number => { + return el.height * el.width; + }; + let largetstSize = getSize(elements[0]); + for (let i = 1; i < elements.length; i++) { + const size = getSize(elements[i]); + if (size > largetstSize) { + largetstSize = size; + largestElement = elements[i]; + } + } + return largestElement; + }, + intersectElementWithLine( + element: ExcalidrawBindableElement, + a: readonly [number, number], + b: readonly [number, number], + gap?: number, + ): Point[] { + return intersectElementWithLine(element, a, b, gap); + }, }; await initFonts(); return window.ExcalidrawAutomate; @@ -1081,11 +1178,12 @@ export function destroyExcalidrawAutomate() { function normalizeLinePoints( points: [[x: number, y: number]], - box: { x: number; y: number; w: number; h: number }, + //box: { x: number; y: number; w: number; h: number }, ) { const p = []; + const [x,y] = points[0]; for (let i = 0; i < points.length; i++) { - p.push([points[i][0] - box.x, points[i][1] - box.y]); + p.push([points[i][0] - x, points[i][1] - y]); } return p; } @@ -1360,16 +1458,17 @@ export async function createSVG( function estimateLineBound(points: any): [number, number, number, number] { let minX = Infinity; - let maxX = -Infinity; let minY = Infinity; + let maxX = -Infinity; let maxY = -Infinity; - points.forEach((p: any) => { - const [x, y] = p; + + 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]; } diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 058f352..ce58f3b 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -1161,11 +1161,13 @@ export default class ExcalidrawView extends TextFileView { ); } //debug({where:"ExcalidrawView.addElements",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene",state:st}) + const elements = el.concat(newElements); this.excalidrawAPI.updateScene({ - elements: el.concat(newElements), + elements, appState: st, commitToHistory: true, }); + if (images) { const files: BinaryFileData[] = []; Object.keys(images).forEach((k) => { @@ -1200,7 +1202,7 @@ export default class ExcalidrawView extends TextFileView { this.excalidrawAPI.addFiles(files); } if (save) { - this.save(); + this.save(false); } else { this.dirty = this.file?.path; } diff --git a/src/Scripts.ts b/src/Scripts.ts index bf1abd7..d4b456e 100644 --- a/src/Scripts.ts +++ b/src/Scripts.ts @@ -70,9 +70,7 @@ export class ScriptEngine { this.scriptPath = this.plugin.settings.scriptFolderPath; const scripts = app.vault .getFiles() - .filter( - (f: TFile) => f.path.startsWith(this.scriptPath), - ); + .filter((f: TFile) => f.path.startsWith(this.scriptPath)); scripts.forEach((f) => this.loadScript(f)); } @@ -101,9 +99,7 @@ export class ScriptEngine { const app = this.plugin.app; const scripts = app.vault .getFiles() - .filter( - (f: TFile) => f.path.startsWith(this.scriptPath), - ); + .filter((f: TFile) => f.path.startsWith(this.scriptPath)); scripts.forEach((f) => this.unloadScript(f.basename)); } @@ -135,7 +131,7 @@ export class ScriptEngine { return await new AsyncFunction("ea", "utils", script)(this.plugin.ea, { inputPrompt: (header: string, placeholder?: string, value?: string) => - ScriptEngine.inputPrompt(this.plugin.app, header, placeholder, value) + ScriptEngine.inputPrompt(this.plugin.app, header, placeholder, value), }); } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 348f601..ab90f25 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -77,7 +77,7 @@ export default { SCRIPT_FOLDER_DESC: "The files you place in this folder will be treated as Excalidraw Automate scripts. " + "You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " + - "hotkeys to your favorite scripts just like to any other Obsidian command. " + + "hotkeys to your favorite scripts just like to any other Obsidian command. " + "The folder may not be the root folder of your Vault. ", AUTOSAVE_NAME: "Autosave", AUTOSAVE_DESC: diff --git a/src/main.ts b/src/main.ts index 5418950..3eaa678 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1032,7 +1032,7 @@ export default class ExcalidrawPlugin extends Plugin { ea.reset(); await ea.addLaTex(0, 0, formula); ea.setView(view); - ea.addElementsToView(true, true); + ea.addElementsToView(true, false); }); return true; } diff --git a/src/settings.ts b/src/settings.ts index 66223fa..9e8d6af 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,10 @@ -import { App, DropdownComponent, normalizePath, PluginSettingTab, Setting } from "obsidian"; +import { + App, + DropdownComponent, + normalizePath, + PluginSettingTab, + Setting, +} from "obsidian"; import { VIEW_TYPE_EXCALIDRAW } from "./constants"; import ExcalidrawView from "./ExcalidrawView"; import { t } from "./lang/helpers"; @@ -203,7 +209,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab { .setPlaceholder("Excalidraw/Scripts") .setValue(this.plugin.settings.scriptFolderPath) .onChange(async (value) => { - this.plugin.settings.scriptFolderPath = normalizePath(value); + this.plugin.settings.scriptFolderPath = normalizePath(value); this.applySettingsUpdate(); }), ); diff --git a/yarn.lock b/yarn.lock index 2e1a068..4de1078 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1202,10 +1202,10 @@ "@typescript-eslint/types" "5.6.0" "eslint-visitor-keys" "^3.0.0" -"@zsviczian/excalidraw@0.10.0-obsidian-14": - "integrity" "sha512-+Rxdbpg7b92FEjgUJBGwV4QEYt1vupbWA09lVmiouv/n3hsEdcz4cjXV15+E8v3wMMOvxpTEsSP1d4SqUZ7XyA==" - "resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-14.tgz" - "version" "0.10.0-obsidian-14" +"@zsviczian/excalidraw@0.10.0-obsidian-18": + "integrity" "sha512-hnlDZUVVOMSgIoKRTu6gGe6mQVg4ggExWqB/DyaWJkv9DoFigMUYvPYA8xz3t1hWqtRHKTWOyA1CiFJFK/Zt8w==" + "resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-18.tgz" + "version" "0.10.0-obsidian-18" dependencies: "dotenv" "10.0.0"