From 0ec6acbeed2ef0293267a0e40dd21934f0f2d31a Mon Sep 17 00:00:00 2001 From: Zsolt Viczian Date: Sun, 2 May 2021 21:30:41 +0200 Subject: [PATCH] 1.0.8 ExcalidrawAutomate --- AutomateHowTo.md | 301 +++++++++++++++++++++++++++++ images/FristDemo.png | Bin 0 -> 23109 bytes images/coordinates.png | Bin 0 -> 20842 bytes manifest.json | 2 +- package.json | 1 + src/ExcalidrawTemplate.ts | 388 ++++++++++++++++++++++++++++++++++++++ src/ExcalidrawView.ts | 4 +- src/main.ts | 92 +++++++-- yarn.lock | 2 +- 9 files changed, 771 insertions(+), 19 deletions(-) create mode 100644 AutomateHowTo.md create mode 100644 images/FristDemo.png create mode 100644 images/coordinates.png create mode 100644 src/ExcalidrawTemplate.ts diff --git a/AutomateHowTo.md b/AutomateHowTo.md new file mode 100644 index 0000000..4a76c61 --- /dev/null +++ b/AutomateHowTo.md @@ -0,0 +1,301 @@ +# Excalidraw Automate How To + +Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://github.com/SilentVoid13/Templater) plugin. + +With a little work, using Excalidraw Automate you can generate simple mindmaps, fill out SVG forms, create customized charts, etc. based on documents in your vault. + +You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code. + +```javascript +const ea = ExcalidrawAutomate; +ea.reset(); +``` + +The first line creates a practical 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. + +## Basic logic of using Excalidraw Automate +1. Set the styling of the elements you want to draw +2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one. +3. Call create to instantiate the drawing + +You can change styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using this. There would be no point in setting all these parameters each time you add an element. + +### Before we dive deeper, here's a simple example script +```javascript +<%* + const ea = ExcalidrawAutomate; + ea.reset(); + ea.addRect(-150,-50,450,300); + await ea.addText(-100,70,"Left to right"); + ea.addArrow([[-100,100],[100,100]]); + + ea.style.strokeColor = "red"; + await ea.addText(100,-30,"top to bottom",200,null,"center"); + ea.addArrow([[200,0],[200,200]]); + await ea.create(); +%> +``` +The script will generate the following drawing: +![[./images/FristDemo.png]] + +## Attributes and functions at a glance +Here's the interface implemented by ExcalidrawAutomate: + +```typescript +ExcalidrawAutomate: { + style: { + strokeColor: string; + backgroundColor: string; + angle: number; + fillStyle: FillStyle; + strokeWidth: number; + storkeStyle: StrokeStyle; + roughness: number; + opacity: number; + strokeSharpness: StrokeSharpness; + fontFamily: FontFamily; + fontSize: number; + textAlign: string; + verticalAlign: string; + startArrowHead: string; + endArrowHead: string; + } + canvas: {theme: string, viewBackgroundColor: string}; + setFillStyle: Function; + setStrokeStyle: Function; + setStrokeSharpness: Function; + setFontFamily: Function; + setTheme: Function; + create: Function; + addRect: Function; + addDiamond: Function; + addEllipse: Function; + addText: Function; + addLine: Function; + addArrow: Function; + connectObjects: Function; + clear: Function; + reset: Function; +}; +``` + +## Element Style +As you will notice, some styles have setter functions. This is to help you navigate the allowed values for the property. You do not need to use the setter function however, you can use set the value directly as well. + +### strokeColor +String. The color of the line. + +Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp) or hexadecimal RGB strings e.g. `#FF0000` for red. + +### backgroundColor +String. This is the fill color of an object. + +Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`. + +### angle +Number. Rotation in radian. 90° == `Math.PI/2`. + +### fillStyle, setFillStyle() +```typescript +type FillStyle = "hachure" | "cross-hatch" | "solid"; +setFillStyle (val:number); +``` +fillStyle is a string. + +`setFillStyle()` accepts a number: +- 0: "hachure" +- 1: "cross-hatch" +- any other number: "solid" + +### strokeWidth +Number, sets the width of the stroke. + +### strokeStyle, setStrokeStyle() +```typescript +type StrokeStyle = "solid" | "dashed" | "dotted"; +setStrokeStyle (val:number); +``` +strokeStyle is a string. + +`setStrokeStyle()` accepts a number: +- 0: "solid" +- 1: "dashed" +- any other number: "dotted" + +### roughness +Number. Called sloppiness in Excalidraw. Three values are accepted: +- 0: Architect +- 1: Artist +- 2: Cartoonist + +### opacity +Number between 0 and 100. The opacity of an object, both stroke and fill. + +### strokeSharpness, setStrokeSharpness() +```typescript +type StrokeSharpness = "round" | "sharp"; +setStrokeSharpness(val:nmuber); +``` +strokeSharpness is a string. + +"round" lines are curvey, "sharp" lines break at the turning point. + +`setStrokeSharpness()` accepts a number: +- 0: "round" +- any other number: "sharp" + +### fontFamily, setFontFamily() +Number. Valid values are 1,2 and 3. + +`setFontFamily()` will also accept a number and return the name of the font. +- 1: "Virgil, Segoe UI Emoji" +- 2: "Helvetica, Segoe UI Emoji" +- 3: "Cascadia, Segoe UI Emoji" + +### fontSize +Number. Default value is 20 px + +### textAlign +String. Alignment of the text horizontally. Valid values are "left", "center", "right". + +This is relevant when setting a fix width using the `addText()` function. + +### verticalAlign +String. Alignment of the text vertically. Valid values are "top" and "middle". + +This is relevant when setting a fix height using the `addText()` function. + +### startArroHead, endArrowHead +String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow. + +This is relavant when using the `addArrow()` and the `connectObjects()` functions. + +## canvas +Sets the properties of the canvas. + +### theme, setTheme() +String. Valid values are "light" and "dark". + +`setTheme()` accepts a number: +- 0: "light" +- any other number: "dark" + +### viewBackgroundColor +String. This is the fill color of an object. + +Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`. + +## Adding objects +These functions will add objects to your drawing. The canvas is infinite, and it accepts negative and positive X and Y values. The top left corner x values increase left to right, y values increase top to bottom. +![[./images/coordinates.png]] + +### addRect(), addDiamond, addEllipse +```typescript +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 +``` +Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. + +### addText +```typescript +async addText(topX:number, topY:number, text:string, width?:number, height?:number,textAlign?: string, verticalAlign?:string):Promise +``` + +Adds text to the drawing. If `width` and `height` are not specified, the function will calculate the width and height based on the fontFamily, the fontSize and the text provided. + +In case you want to position a text in the center compared to other elements on the drawing, you can provide a fixed height and width, and you can also specify `textAlign` and `verticalAlign` as described above. + +Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. + +This is an asynchronous function. It must be called with an `await` from the Templater script, otherwise the text will not appear. See code example above. + +### addLine() +```typescript +addLine(points: [[x:number,y:number]]):void +``` +Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round". + +### addArrow() +```typescript +addArrow(points: [[x:number,y:number]],startArrowHead?:string,endArrowHead?:string,startBinding?:string,endBinding?:string):void +``` + +Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round". + +`startArrowHead` and `endArrowHead` specify the type of arrow head to use, as described above. Valid values are "none", "arrow", "dot", and "bar". + +`startBinding` and `endBinding` are the object id's of connected objects. Do not use directly with this function. Use `connectObjects` instead. + +### connectObjects() +```typescript +declare type ConnectionPoint = "top"|"bottom"|"left"|"right"; + +connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, numberOfPoints: number = 1,startArrowHead?:string,endArrowHead?:string):void +``` +Connects two objects with an arrow. + +`objectA` and `objectB` are strings. These are the ids of the objects to connect. IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating these objects. + +`connectionA` and `connectionB` specify where to connect on the object. Valid values are: "top", "bottom", "left", and "right". + +`numberOfPoints` set the number of interim break points for the line. Default value is one, meaning there will be 1 breakpoint between the start and the end points of the arrow. When moving objects on the drawing, these breakpoints will influence how the line is rerouted by Excalidraw. + +`startArrowHead` and `endArrowHead` function as described for `addArrow()` above. + +## Utility functions +### clear() +`clear()` will clear objects from cache, but will retain element style settings. + +### reset() +`reset()` will first call `clear()` and then reset element style to defaults. + +### create() +```typescript +async create(filename?: string, foldername?:string, templatePath?:string, onNewPane: boolean = false) +``` +Creates the drawing and opens it. + +`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename. + +`foldername` is the folder where the file should be created. If `null` then the default folder for new drawings will be used according to Excalidraw settings. + +`templatePath` the filename including full path and extension for a template file to use. This template file will be added as the base layer, all additional objects added via ExcalidrawAutomate will appear on top of elements in the template. If `null` then no template will be used, i.e. an empty white drawing will be the base for adding objects. + +`onNewPane` defines where the new drawing should be created. `false` will open the drawing on the current active leaf. `true` will open the drawing by vertically splitting the current leaf. + +## Examples +### Connect objects +```javascript +<%* + const ea = ExcalidrawAutomate; + ea.reset(); + await ea.addText(-130,-100,"Connecting two objects"); + const a = ea.addRect(-100,-100,100,100); + const b = ea.addEllipse(200,200,100,100); + ea.connectObjects(a,"bottom",b,"left",2); //see how the line breaks differently when moving objects around + ea.style.strokeColor = "red"; + ea.connectObjects(a,"right",b,"top",1); + await ea.create(); +%> +``` +### Using a template +This example is similar to the first one, but rotated 90°, and using a template, plus specifying a filename and folder to save the drawing, and opening the new drawing in a new pane. +```javascript +<%* + const ea = ExcalidrawAutomate; + ea.reset(); + ea.style.angle = Math.PI/2; + ea.style.strokeWidth = 3.5; + ea.addRect(-150,-50,450,300); + await ea.addText(-100,70,"Left to right"); + ea.addArrow([[-100,100],[100,100]]); + + ea.style.strokeColor = "red"; + await ea.addText(100,-30,"top to bottom",200,null,"center"); + ea.addArrow([[200,0],[200,200]]); + await ea.create("My Drawing","myfolder/fordemo/","Excalidraw/Template2.excalidraw",true); +%> +``` \ No newline at end of file diff --git a/images/FristDemo.png b/images/FristDemo.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b27b597943323ed4a7757aba07c380e9417110 GIT binary patch literal 23109 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 qBT#AehC!-$5fY!)N=55&r`ZVGlR} literal 0 HcmV?d00001 diff --git a/images/coordinates.png b/images/coordinates.png new file mode 100644 index 0000000000000000000000000000000000000000..aea0c995b82c1a2331648242b922df2826cabd6a GIT binary patch literal 20842 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 X{C|5@dhWv+iWuS|GWW9YXnX%Z6HVn} literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json index 74b06be..3c5d8f0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "1.0.7", + "version": "1.0.8", "minAppVersion": "0.11.13", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/package.json b/package.json index e6309bb..b93cf74 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/node": "^14.14.2", "@types/react-dom": "^17.0.0", "cross-env": "7.0.3", + "nanoid": "3.1.22", "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", "postcss": "^8.2.6", "rollup": "2.45.2", diff --git a/src/ExcalidrawTemplate.ts b/src/ExcalidrawTemplate.ts new file mode 100644 index 0000000..199b015 --- /dev/null +++ b/src/ExcalidrawTemplate.ts @@ -0,0 +1,388 @@ +import ExcalidrawPlugin from "./main"; +import { + ExcalidrawElement, + FillStyle, + StrokeStyle, + StrokeSharpness, + FontFamily, +} from "@excalidraw/excalidraw/types/element/types"; +import {nanoid} from "nanoid"; +import { + normalizePath, + parseFrontMatterAliases, + TFile +} from "obsidian" + +declare type ConnectionPoint = "top"|"bottom"|"left"|"right"; + +export interface ExcalidrawAutomate extends Window { + ExcalidrawAutomate: { + plugin: ExcalidrawPlugin; + elementIds: []; + elementsDict: {}, + style: { + strokeColor: string; + backgroundColor: string; + angle: number; + fillStyle: FillStyle; + strokeWidth: number; + storkeStyle: StrokeStyle; + roughness: number; + opacity: number; + strokeSharpness: StrokeSharpness; + fontFamily: FontFamily; + fontSize: number; + textAlign: string; + verticalAlign: string; + startArrowHead: string; + endArrowHead: string; + } + canvas: {theme: string, viewBackgroundColor: string}; + setFillStyle: Function; + setStrokeStyle: Function; + setStrokeSharpness: Function; + setFontFamily: Function; + setTheme: Function; + create: Function; + addRect: Function; + addDiamond: Function; + addEllipse: Function; + addText: Function; + addLine: Function; + addArrow: Function; + connectObjects: Function; + clear: Function; + reset: Function; + }; +} + +declare let window: ExcalidrawAutomate; + +export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) { + window.ExcalidrawAutomate = { + plugin: plugin, + elementIds: [], + elementsDict: {}, + style: { + strokeColor: "#000000", + backgroundColor: "transparent", + angle: 0, + fillStyle: "hachure", + strokeWidth:1, + storkeStyle: "solid", + roughness: 1, + opacity: 100, + strokeSharpness: "sharp", + fontFamily: 1, + fontSize: 20, + textAlign: "left", + verticalAlign: "top", + startArrowHead: null, + endArrowHead: "arrow" + }, + canvas: {theme: "light", viewBackgroundColor: "#FFFFFF"}, + setFillStyle (val:number) { + switch(val) { + case 0: + this.style.fillStyle = "hachure"; + return "hachure"; + case 1: + this.style.fillStyle = "cross-hatch"; + return "cross-hatch"; + default: + this.style.fillStyle = "solid"; + return "solid"; + } + }, + setStrokeStyle (val:number) { + switch(val) { + case 0: + this.style.strokeStyle = "solid"; + return "solid"; + case 1: + this.style.strokeStyle = "dashed"; + return "dashed"; + default: + this.style.strokeStyle = "dotted"; + return "dotted"; + } + }, + setStrokeSharpness (val:number) { + switch(val) { + case 0: + this.style.strokeSharpness = "round"; + return "round"; + default: + this.style.strokeSharpness = "sharp"; + return "sharp"; + } + }, + setFontFamily (val:number) { + switch(val) { + case 1: + this.style.fontFamily = 1; + return getFontFamily(1); + case 2: + this.style.fontFamily = 2; + return getFontFamily(2); + default: + this.style.strokeSharpness = 3; + return getFontFamily(3); + } + }, + setTheme (val:number) { + switch(val) { + case 0: + this.canvas.theme = "light"; + return "light"; + default: + this.canvas = "dark"; + return "dark"; + } + }, + async create(filename?: string, foldername?:string, templatePath?:string, onNewPane: boolean = false) { + let elements = templatePath ? (await getTemplate(templatePath)).elements : []; + for (let i=0;i{ + return JSON.stringify({ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": elements, + "appState": { + "theme": this.canvas.theme, + "viewBackgroundColor": this.canvas.viewBackgroundColor + } + }); + })()); + }, + addRect(topX:number, topY:number, width:number, height:number):string { + const id = nanoid(); + this.elementIds.push(id); + this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height); + return id; + }, + addDiamond(topX:number, topY:number, width:number, height:number):string { + const id = nanoid(); + this.elementIds.push(id); + this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height); + return id; + }, + addEllipse(topX:number, topY:number, width:number, height:number):string { + const id = nanoid(); + this.elementIds.push(id); + this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height); + return id; + }, + async addText(topX:number, topY:number, text:string, width?:number, height?:number,textAlign?: string, verticalAlign?:string):Promise { + const id = nanoid(); + const {w, h, baseline} = await measureText(text); + this.elementIds.push(id); + this.elementsDict[id] = { + text: text, + fontSize: window.ExcalidrawAutomate.style.fontSize, + fontFamily: window.ExcalidrawAutomate.style.fontFamily, + textAlign: textAlign ? textAlign : window.ExcalidrawAutomate.style.textAlign, + verticalAlign: verticalAlign ? verticalAlign : window.ExcalidrawAutomate.style.verticalAlign, + baseline: baseline, + ... boxedElement(id,"text",topX,topY,width ? width:w, height ? height:h) + }; + return id; + }, + addLine(points: [[x:number,y:number]]):void { + const box = getLineBox(points); + const id = nanoid(); + this.elementIds.push(id); + this.elementsDict[id] = { + points: normalizeLinePoints(points), + lastCommittedPoint: null, + startBinding: null, + endBinding: null, + startArrowhead: null, + endArrowhead: null, + ... boxedElement(id,"line",box.x,box.y,box.w,box.h) + }; + }, + addArrow(points: [[x:number,y:number]],startArrowHead?:string,endArrowHead?:string,startBinding?:string,endBinding?:string):void { + const box = getLineBox(points); + const id = nanoid(); + this.elementIds.push(id); + this.elementsDict[id] = { + points: normalizeLinePoints(points), + lastCommittedPoint: null, + startBinding: {elementId:startBinding,focus:0.1,gap:4}, + endBinding: {elementId:endBinding,focus:0.1,gap:4}, + startArrowhead: startArrowHead ? startArrowHead : this.style.startArrowHead, + endArrowhead: endArrowHead ? endArrowHead : this.style.endArrowHead, + ... boxedElement(id,"arrow",box.x,box.y,box.w,box.h) + }; + if(startBinding) this.elementsDict[startBinding].boundElementIds.push(id); + if(endBinding) this.elementsDict[endBinding].boundElementIds.push(id); + }, + connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, numberOfPoints: number = 1,startArrowHead?:string,endArrowHead?:string):void { + if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) { + return; + } + const getSidePoints = (side:string, el:any) => { + switch(side) { + case "bottom": + return [((el.x) + (el.x+el.width))/2, el.y+el.height]; + case "left": + return [el.x, ((el.y) + (el.y+el.height))/2]; + case "right": + return [el.x+el.width, ((el.y) + (el.y+el.height))/2]; + default: //"top" + return [((el.x) + (el.x+el.width))/2, el.y]; + } + } + const [aX, aY] = getSidePoints(connectionA,this.elementsDict[objectA]); + const [bX, bY] = getSidePoints(connectionB,this.elementsDict[objectB]); + const numAP = numberOfPoints+2; //number of break points plus the beginning and the end + let points = []; + for(let i=0;i points[i][0]) ? rightX : points[i][0]; + bottomY = (bottomY > points[i][1]) ? bottomY : points[i][1]; + } + return { + x: leftX, + y: topY, + w: rightX-leftX, + h: bottomY-topY + }; +} + +function getFontFamily(id:number) { + switch (id) { + case 1: return "Virgil, Segoe UI Emoji"; + case 2: return "Helvetica, Segoe UI Emoji"; + case 3: return "Cascadia, Segoe UI Emoji"; + } +} + +async function measureText (newText:string) { + const line = document.createElement("div"); + const body = document.body; + line.style.position = "absolute"; + line.style.whiteSpace = "pre"; + line.style.font = window.ExcalidrawAutomate.style.fontSize.toString()+'px ' + + getFontFamily(window.ExcalidrawAutomate.style.fontFamily); + await (document as any).fonts.load(line.style.font); + body.appendChild(line); + line.innerText = newText + .split("\n") + // replace empty lines with single space because leading/trailing empty + // lines would be stripped from computation + .map((x) => x || " ") + .join("\n"); + const width = line.offsetWidth; + const height = line.offsetHeight; + // Now creating 1px sized item that will be aligned to baseline + // to calculate baseline shift + const span = document.createElement("span"); + span.style.display = "inline-block"; + span.style.overflow = "hidden"; + span.style.width = "1px"; + span.style.height = "1px"; + line.appendChild(span); + // Baseline is important for positioning text on canvas + const baseline = span.offsetTop + span.offsetHeight; + document.body.removeChild(line); + return {w: width, h: height, baseline: baseline }; +}; + +async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> { + const vault = window.ExcalidrawAutomate.plugin.app.vault; + const file = vault.getAbstractFileByPath(normalizePath(fileWithPath)); + if(file && file instanceof TFile) { + const data = await vault.read(file); + const excalidrawData = JSON.parse(data); + return { + elements: excalidrawData.elements, + appState: excalidrawData.appState, + }; + }; + return { + elements: [], + appState: {}, + } +} diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 71b06f1..28aa83a 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -110,7 +110,7 @@ export default class ExcalidrawView extends TextFileView { async onunload() { if(this.excalidrawRef) await this.save(); } - + setViewData (data: string, clear: boolean) { if (this.app.workspace.layoutReady) { this.loadDrawing(data,clear); @@ -127,7 +127,7 @@ export default class ExcalidrawView extends TextFileView { ReactDOM.unmountComponentAtNode(this.contentEl); } } - + private async loadDrawing (data:string, clear:boolean) { if(clear) this.clear(); this.justLoaded = true; //a flag to trigger zoom to fit after the drawing has been loaded diff --git a/src/main.ts b/src/main.ts index 216168b..81953b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,8 @@ import { MarkdownView, normalizePath, MarkdownPostProcessorContext, + Menu, + MenuItem, } from 'obsidian'; import { BLANK_DRAWING, @@ -23,7 +25,9 @@ import { PNG_ICON_NAME, SVG_ICON, SVG_ICON_NAME, - RERENDER_EVENT + RERENDER_EVENT, + VIRGIL_FONT, + CASCADIA_FONT } from './constants'; import ExcalidrawView, {ExportSettings} from './ExcalidrawView'; import { @@ -35,12 +39,24 @@ import { openDialogAction, OpenFileDialog } from './openDrawing'; +import { + initExcalidrawAutomate, + destroyExcalidrawAutomate +} from './ExcalidrawTemplate'; +import { norm } from '@excalidraw/excalidraw/types/ga'; +export interface ExcalidrawAutomate extends Window { + ExcalidrawAutomate: { + theme: string; + createNew: Function; + }; +} export default class ExcalidrawPlugin extends Plugin { public settings: ExcalidrawSettings; private openDialog: OpenFileDialog; - + private excalidrawAutomate: ExcalidrawAutomate; + constructor(app: App, manifest: PluginManifest) { super(app, manifest); } @@ -51,6 +67,13 @@ export default class ExcalidrawPlugin extends Plugin { addIcon(PNG_ICON_NAME,PNG_ICON); addIcon(SVG_ICON_NAME,SVG_ICON); + const myFonts = document.createElement('style'); + myFonts.appendChild(document.createTextNode(VIRGIL_FONT)); + myFonts.appendChild(document.createTextNode(CASCADIA_FONT)); + document.head.appendChild(myFonts); + + initExcalidrawAutomate(this); + this.registerView( VIEW_TYPE_EXCALIDRAW, (leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this) @@ -154,6 +177,21 @@ export default class ExcalidrawPlugin extends Plugin { } }, }); + + this.registerEvent( + this.app.workspace.on("file-menu", (menu: Menu, file: TFile) => { + if (file instanceof TFolder) { + menu.addItem((item: MenuItem) => { + item.setTitle("Create Excalidraw drawing") + .setIcon(ICON_NAME) + .onClick(evt => { + this.createDrawing(file.path+this.getNextDefaultFilename(),false,file.path); + }) + }); + } + }) + ); + //watch filename change to rename .svg this.app.vault.on('rename',async (file,oldPath) => { if (!(this.settings.keepInSync && file instanceof TFile)) return; @@ -168,16 +206,34 @@ export default class ExcalidrawPlugin extends Plugin { //watch file delete and delete corresponding .svg this.app.vault.on('delete',async (file:TFile) => { - if (!(this.settings.keepInSync && file instanceof TFile)) return; + if (!(file instanceof TFile)) return; if (file.extension != EXCALIDRAW_FILE_EXTENSION) return; - const svgPath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg'; - const svgFile = this.app.vault.getAbstractFileByPath(normalizePath(svgPath)); - if(svgFile && svgFile instanceof TFile) { - await this.app.vault.delete(svgFile); + + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); + for (let i=0;i { el.createDiv("excalidraw-error",(el)=> { @@ -264,11 +320,11 @@ export default class ExcalidrawPlugin extends Plugin { } public async openDrawing(drawingFile: TFile, onNewPane: boolean) { - const leafs = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); let leaf:WorkspaceLeaf = null; - if (leafs?.length > 0) { - leaf = leafs[0]; + if (leaves?.length > 0) { + leaf = leaves[0]; } if(!leaf) { leaf = this.app.workspace.activeLeaf; @@ -289,21 +345,27 @@ export default class ExcalidrawPlugin extends Plugin { } private getNextDefaultFilename():string { - return this.settings.folder+'/Drawing ' + window.moment().format('YYYY-MM-DD HH.mm.ss')+'.'+EXCALIDRAW_FILE_EXTENSION; + return 'Drawing ' + window.moment().format('YYYY-MM-DD HH.mm.ss')+'.'+EXCALIDRAW_FILE_EXTENSION; } - public async createDrawing(filename: string, onNewPane: boolean) { - const folder = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.folder)); + public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) { + const fname = foldername ? normalizePath(foldername)+'/'+filename : normalizePath(this.settings.folder) + '/' + filename; + const folder = this.app.vault.getAbstractFileByPath(normalizePath(foldername ? foldername: this.settings.folder)); if (!(folder && folder instanceof TFolder)) { await this.app.vault.createFolder(this.settings.folder); } + if(initData) { + this.openDrawing(await this.app.vault.create(fname,initData),onNewPane); + return; + } + const file = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath)); if(file && file instanceof TFile) { const content = await this.app.vault.read(file); - this.openDrawing(await this.app.vault.create(filename,content==''?BLANK_DRAWING:content), onNewPane); + this.openDrawing(await this.app.vault.create(fname,content==''?BLANK_DRAWING:content), onNewPane); } else { - this.openDrawing(await this.app.vault.create(filename,BLANK_DRAWING), onNewPane); + this.openDrawing(await this.app.vault.create(fname,BLANK_DRAWING), onNewPane); } } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2cc79a3..5815342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7650,7 +7650,7 @@ "dns-packet" "^1.3.1" "thunky" "^1.0.2" -"nanoid@^3.1.20", "nanoid@^3.1.22": +"nanoid@^3.1.20", "nanoid@^3.1.22", "nanoid@3.1.22": "integrity" "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz" "version" "3.1.22"