mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
32 Commits
1.0.9
...
1.0.10-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7cf04cef8 | ||
|
|
0cb4c0c4d2 | ||
|
|
f13c5a2df5 | ||
|
|
f5e0d56d99 | ||
|
|
7ac04c0f74 | ||
|
|
934eea794b | ||
|
|
045ee288d5 | ||
|
|
7455405425 | ||
|
|
9c52a7d851 | ||
|
|
c9a7930a04 | ||
|
|
fdee55ccf0 | ||
|
|
b4d9469b7d | ||
|
|
caecd7422b | ||
|
|
7ed33646e0 | ||
|
|
d491e24605 | ||
|
|
65baf16d8a | ||
|
|
5bc756288c | ||
|
|
dd41cd1eeb | ||
|
|
0e8ff2f5cf | ||
|
|
aa78e7ea54 | ||
|
|
18c0badc25 | ||
|
|
34f08766f4 | ||
|
|
8cbc0f1d53 | ||
|
|
dc8223b6fa | ||
|
|
b445a62f50 | ||
|
|
3dcc156e46 | ||
|
|
ba5c132f17 | ||
|
|
a6671ff35b | ||
|
|
3a48db940d | ||
|
|
524626cb5b | ||
|
|
bd06d08071 | ||
|
|
76ca98e3ed |
@@ -6,7 +6,7 @@ With a little work, using Excalidraw Automate you can generate simple mindmaps,
|
||||
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code.
|
||||
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
@@ -27,7 +27,7 @@ You can change styling between adding different elements. My logic for separatin
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -42,7 +42,7 @@ Use CTRL+Shift+V to paste it into Obsidian!
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -63,7 +63,7 @@ The script will generate the following drawing:
|
||||
|
||||
## Attributes and functions at a glance
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
ExcalidrawAutomate: {
|
||||
style: {
|
||||
@@ -321,28 +321,31 @@ async createPNG(templatePath?:string)
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
|
||||
## Examples
|
||||
### Create new drawing and insert into currently edited document
|
||||
### Insert new drawing into currently edited document
|
||||
This template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
defaultTitle = tp.file.title;
|
||||
title = await tp.system.prompt("Title of the new drawing?",defaultTitle);
|
||||
tR = String.fromCharCode(96,96,96)+'excalidraw\n[['+title+'.excalidraw]]\n'+String.fromCharCode(96,96,96)
|
||||
const defaultTitle = tp.date.now("HHmm")+' '+tp.file.title;
|
||||
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
|
||||
const folder = tp.file.folder(true);
|
||||
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
|
||||
tR = String.fromCharCode(96,96,96)+'excalidraw\n[['+transcludePath+']]\n'+String.fromCharCode(96,96,96);
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
await ea.create({
|
||||
filename : title,
|
||||
foldername : tp.file.folder(true),
|
||||
template : 'Excalidraw/Template.excalidraw',
|
||||
onNewPane : true
|
||||
filename : title,
|
||||
foldername : folder,
|
||||
//templatePath: 'Excalidraw/Template.excalidraw', //uncomment if you want to use a template
|
||||
onNewPane : true
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
### Connect objects
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -359,7 +362,7 @@ Use CTRL+Shift+V to paste it into Obsidian!
|
||||
### 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.
|
||||
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -399,7 +402,7 @@ Example input:
|
||||
|
||||
The script:
|
||||
|
||||
Use CTRL+Shift+V to paste it into Obsidian!
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
|
||||
@@ -18,7 +18,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
|
||||
- The plugin saves drawings to your vault as a file with the *.excalidraw* file extension.
|
||||
- You can customize the **size and position of the embedded image** using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added as style to the SVG element and the wrapper DIV element. Check below and styles.css for more insight.
|
||||
- You can setup Excalidraw to **automatically export SVG and/or PNG** files for your drawings, and to keep those in sync with your drawing.
|
||||
- Includes full [Templater](https://github.com/SilentVoid13/Templater) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/AutomateHowTo.md)
|
||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
|
||||
## How to?
|
||||
Part 1: Intro to Obsidian-Excalidraw - Start a new drawing (3:12)
|
||||
@@ -84,9 +84,9 @@ div.excalidraw-svg-left {
|
||||
- Saves drawing when exiting Obsidian
|
||||
- Fixes pen positioning bug with sliding panes after panes scroll
|
||||
|
||||
### ExcalidrawAutomte full Templater support
|
||||
You now have ultimate flexibility over your Excalidraw templates using Templater.
|
||||
- Detailed documentation available [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/AutomateHowTo.md)
|
||||
### ExcalidrawAutomte full Templater and DataviewJS support
|
||||
You now have ultimate flexibility over your Excalidraw templates using Templater and Dataview.
|
||||
- Detailed documentation available [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
- I created few examples from the simple to the more complex
|
||||
- Simple use-case: Creating a drawing using a custom template and following a file and folder naming convention of your choice.
|
||||
- Complex use-case: Create a mindmap from a tabulated outline.
|
||||
|
||||
45
docs/API/attributes_functions_overview.md
Normal file
45
docs/API/attributes_functions_overview.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Attributes and functions overivew
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```javascript
|
||||
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;
|
||||
addRect: Function;
|
||||
addDiamond: Function;
|
||||
addEllipse: Function;
|
||||
addText: Function;
|
||||
addLine: Function;
|
||||
addArrow: Function;
|
||||
connectObjects: Function;
|
||||
addToGroup: Function;
|
||||
toClipboard: Function;
|
||||
create: Function;
|
||||
createPNG: Function;
|
||||
createSVG: Function;
|
||||
clear: Function;
|
||||
reset: Function;
|
||||
};
|
||||
```
|
||||
15
docs/API/canvas_style.md
Normal file
15
docs/API/canvas_style.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Canvas style settings
|
||||
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. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
91
docs/API/element_style.md
Normal file
91
docs/API/element_style.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Element style settings
|
||||
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. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings, or e.g. `#FF0000` for red.
|
||||
|
||||
### backgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
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.
|
||||
|
||||
### startArrowHead, 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.
|
||||
58
docs/API/introduction.md
Normal file
58
docs/API/introduction.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Introduction to the API
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```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 `await ea.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 are two a simple example scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
await ea.create({
|
||||
filename : tp.date.now("HH.mm"),
|
||||
foldername : tp.date.now("YYYY-MM-DD"),
|
||||
templatePath: "Excalidraw/Template1.excalidraw",
|
||||
onNewPane : false
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
ea.addText(100,-30,"top to bottom",{width:200,textAligh:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
The script will generate the following drawing:
|
||||
|
||||

|
||||
65
docs/API/objects.md
Normal file
65
docs/API/objects.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Adding objects
|
||||
These functions will add objects to your drawing. The canvas is infinite, and it accepts negative and positive X and Y values. X values increase left to right, Y values increase top to bottom.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string
|
||||
```
|
||||
|
||||
Adds text to the drawing.
|
||||
|
||||
Formatting parameters are optional:
|
||||
- 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. e.g.: `{width:500, textAlign:"center"}`
|
||||
- If you want to add a box around the text, set `{box:true}`
|
||||
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:true}` then returns the id of the enclosing box.
|
||||
|
||||
### 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]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId: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 element `style.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". e.g. `{startArrowHead: "dot", endArrowHead: "arrow"}`
|
||||
|
||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||
|
||||
### connectObjects()
|
||||
```typescript
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void
|
||||
```
|
||||
Connects two objects with an arrow.
|
||||
|
||||
`objectA` and `objectB` are strings. These are the ids of the objects to connect. These IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating those 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 zero, meaning there will be no breakpoint in 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` work as described for `addArrow()` above.
|
||||
|
||||
### addToGroup()
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):void
|
||||
```
|
||||
Groups objects listed in `objectIds`.
|
||||
43
docs/API/utility.md
Normal file
43
docs/API/utility.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## 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.
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an exising drawing.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
```
|
||||
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.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
create({filename:"my drawing", foldername:"myfolder/subfolder/", templatePath: "Excalidraw/template.excalidraw", onNewPane: true});
|
||||
```
|
||||
### createSVG()
|
||||
```typescript
|
||||
async createSVG(templatePath?:string)
|
||||
```
|
||||
Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
25
docs/Examples/apply_template.md
Normal file
25
docs/Examples/apply_template.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Applying an Excalidraw Template to a New Drawing
|
||||
This example is similar to the one in the introduction, only 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.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.style.angle = Math.PI/2;
|
||||
ea.style.strokeWidth = 3.5;
|
||||
ea.addRect(-150,-50,450,300);
|
||||
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",{width:200,textAlign:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create({
|
||||
filename :"My Drawing",
|
||||
foldername :"myfolder/fordemo/",
|
||||
templatePath:"Excalidraw/Template2.excalidraw",
|
||||
onNewPane :true});
|
||||
%>
|
||||
```
|
||||
18
docs/Examples/connect_objects.md
Normal file
18
docs/Examples/connect_objects.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Connect Objects
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template demonstrates how to connect two objects using ExcalidrawAutomate.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
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",{numberOfPoints: 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();
|
||||
%>
|
||||
```
|
||||
67
docs/Examples/dataviewjs_familytree.md
Normal file
67
docs/Examples/dataviewjs_familytree.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Family tree from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using dataviewjs, but the output is rendered vertically.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Task List looks like:
|
||||
```markdown
|
||||
- [ ] OBSIDIAN
|
||||
- [ ] Silver
|
||||
- [ ] PawPaw Silv
|
||||
- [ ] MawMaw Silv
|
||||
- [ ] Licat
|
||||
- [ ] PeePaw Li
|
||||
- [ ] MeeMaw Li
|
||||
```
|
||||
|
||||
### dataviewjs script
|
||||
Code to render the excalidraw looks like:
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
let task;
|
||||
|
||||
for (let i = 0; i < subtasks.length; i++) {
|
||||
task = subtasks[i]
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
task["objectID"] = ea.addText((task.size/2+offset)*width,depth*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"top",task.objectID,"bottom",{startArrowHead: 'arrow', endArrowHead: 'dot'});
|
||||
if (i >= 1) {
|
||||
ea.connectObjects(subtasks[i-1]['objectID'],"right",task.objectID,"left", {endArrowHead: 'none'});
|
||||
}
|
||||
|
||||
buildMindmap(task.subtasks, depth-1,offset,task.objectID);
|
||||
offset += task.size/1.5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
```
|
||||
60
docs/Examples/dataviewjs_mindmap.md
Normal file
60
docs/Examples/dataviewjs_mindmap.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Mindmap from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using templater, but because dataview already returns tasks in a tree, it is slightly simpler
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
The input file is `Demo.md` with the following contents:
|
||||
```markdown
|
||||
- [ ] Root task
|
||||
- [ ] task 1.1
|
||||
- [ ] task 1.2
|
||||
- [ ] task 1.2.1
|
||||
- [ ] task 1.2.2
|
||||
- [ ] task 1.3
|
||||
- [ ] task 1.3.1
|
||||
```
|
||||
|
||||
### dataviewjs script
|
||||
The `dataviewjs` script looks like this:
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
for (let task of subtasks) {
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
task["objectID"] = ea.addText(depth*width,(task.size/2+offset)*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"right",task.objectID,"left",{startArrowHead: 'dot'});
|
||||
buildMindmap(task.subtasks, depth+1,offset,task.objectID);
|
||||
offset += task.size;
|
||||
}
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
||||
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
```
|
||||
23
docs/Examples/insert_new_drawing.md
Normal file
23
docs/Examples/insert_new_drawing.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Insert new drawing into currently edited document
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const defaultTitle = tp.date.now("HHmm")+' '+tp.file.title;
|
||||
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
|
||||
const folder = tp.file.folder(true);
|
||||
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
|
||||
tR = String.fromCharCode(96,96,96)+'excalidraw\n[['+transcludePath+']]\n'+String.fromCharCode(96,96,96);
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
await ea.create({
|
||||
filename : title,
|
||||
foldername : folder,
|
||||
//templatePath: 'Excalidraw/Template.excalidraw', //uncomment if you want to use a template
|
||||
onNewPane : true
|
||||
});
|
||||
%>
|
||||
```
|
||||
97
docs/Examples/templater_mindmap.md
Normal file
97
docs/Examples/templater_mindmap.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Example input:
|
||||
```
|
||||
- Test 1
|
||||
- Test 1.1
|
||||
- Test 2
|
||||
- Test 2.1
|
||||
- Test 2.2
|
||||
- Test 2.2.1
|
||||
- Test 2.2.2
|
||||
- Test 2.2.3
|
||||
- Test 2.2.3.1
|
||||
- Test 3
|
||||
- Test 3.1
|
||||
```
|
||||
|
||||
### Templater script
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
|
||||
//check if an editor is the active view
|
||||
const editor = this.app.workspace.activeLeaf?.view?.editor;
|
||||
if(!editor) return;
|
||||
|
||||
//initialize the tree with the title of the document as the first element
|
||||
let tree = [[0,this.app.workspace.activeLeaf?.view?.getDisplayText(),-1,0,[],0]];
|
||||
const linecount = editor.lineCount();
|
||||
|
||||
//helper function, use regex to calculate indentation depth, and to get line text
|
||||
function getLineProps (i) {
|
||||
props = editor.getLine(i).match(/^(\t*)-\s+(.*)/);
|
||||
return [props[1].length+1, props[2]];
|
||||
}
|
||||
|
||||
//a vector that will hold last valid parent for each depth
|
||||
let parents = [0];
|
||||
|
||||
//load outline into tree
|
||||
for(i=0;i<linecount;i++) {
|
||||
[depth,text] = getLineProps(i);
|
||||
if(depth>parents.length) parents.push(i+1);
|
||||
else parents[depth] = i+1;
|
||||
tree.push([depth,text,parents[depth-1],1,[]]);
|
||||
tree[parents[depth-1]][IDX.children].push(i+1);
|
||||
}
|
||||
|
||||
//recursive function to crawl the tree and identify height aka. size of each node
|
||||
function crawlTree(i) {
|
||||
if(i>linecount) return 0;
|
||||
size = 0;
|
||||
if((i+1<=linecount && tree[i+1][IDX.depth] <= tree[i][IDX.depth])|| i == linecount) { //I am a leaf
|
||||
tree[i][IDX.size] = 1;
|
||||
return 1;
|
||||
}
|
||||
tree[i][IDX.children].forEach((node)=>{
|
||||
size += crawlTree(node);
|
||||
});
|
||||
tree[i][IDX.size] = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
crawlTree(0);
|
||||
|
||||
//Build the mindmap in Excalidraw
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
//stores position offset of branch/leaf in height units
|
||||
offsets = [0];
|
||||
|
||||
for(i=0;i<=linecount;i++) {
|
||||
depth = tree[i][IDX.depth];
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{box:true});
|
||||
//set child offset equal to parent offset
|
||||
if((depth+1)>offsets.length) offsets.push(offsets[depth]);
|
||||
else offsets[depth+1] = offsets[depth];
|
||||
offsets[depth] += tree[i][IDX.size];
|
||||
if(tree[i][IDX.parent]!=-1) {
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
await ea.create({onNewPane: true});
|
||||
%>
|
||||
```
|
||||
1
docs/_config.yml
Normal file
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-leap-day
|
||||
35
docs/readme.md
Normal file
35
docs/readme.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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/)
|
||||
|
||||
With a little work, using Excalidraw Automate you can generate simple mindmaps, build a faimly tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
|
||||

|
||||
|
||||
## API documenation
|
||||
- [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)
|
||||
|
||||
|
||||
## Examples
|
||||
- **Templater**
|
||||
- [Insert new drawing into currently edited document](Examples/insert_new_drawing.md)
|
||||
- [Connect objects](Examples/connect_objects.md)
|
||||
- [Apply an Excalidraw template](Examples/apply_template.md)
|
||||
- [Mindmap with Templater](Examples/templater_mindmap.md)
|
||||
|
||||
- **Dataview**
|
||||
- [Mindmap with Dataview](Examples/dataviewjs_mindmap.md)
|
||||
- [Family tree with Dataview](Examples/dataviewjs_familytree.md)
|
||||
|
||||
## If you are enjoying the Obsidian Excalidraw Plugin...
|
||||
Help spread the word by sharing about the Plugin on social media.
|
||||
|
||||
You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on my blog [zsolt.blog](https://zsolt.blog).
|
||||
|
||||
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="150">](https://ko-fi.com/zsolt)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"minAppVersion": "0.11.13",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -141,7 +141,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
this.canvas.theme = "light";
|
||||
return "light";
|
||||
default:
|
||||
this.canvas = "dark";
|
||||
this.canvas.theme = "dark";
|
||||
return "dark";
|
||||
}
|
||||
},
|
||||
@@ -152,7 +152,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
});
|
||||
},
|
||||
async toClipboard(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
@@ -163,7 +164,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
}));
|
||||
},
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
|
||||
let elements = params?.templatePath ? (await getTemplate(params.templatePath)).elements : [];
|
||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
@@ -177,14 +179,15 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
async createSVG(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
@@ -195,8 +198,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
||||
}
|
||||
}),
|
||||
{
|
||||
@@ -206,7 +209,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
)
|
||||
},
|
||||
async createPNG(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
@@ -217,8 +221,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw, {exportToSvg} from "@excalidraw/excalidraw";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
@@ -41,6 +41,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private excalidrawRef: React.MutableRefObject<any>;
|
||||
private justLoaded: boolean;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: boolean;
|
||||
private autosaveTimer: any;
|
||||
private previousSceneVersion: number;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
@@ -49,6 +52,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.excalidrawRef = null;
|
||||
this.plugin = plugin;
|
||||
this.justLoaded = false;
|
||||
this.dirty = false;
|
||||
this.autosaveTimer = null;
|
||||
this.previousSceneVersion = 0;
|
||||
}
|
||||
|
||||
public async saveSVG(data?: string) {
|
||||
@@ -111,15 +117,31 @@ export default class ExcalidrawView extends TextFileView {
|
||||
});
|
||||
this.addAction(PNG_ICON_NAME,"Export as PNG",async (ev)=>this.savePNG());
|
||||
this.addAction(SVG_ICON_NAME,"Export as SVG",async (ev)=>this.saveSVG());
|
||||
//this is to solve sliding panes bug
|
||||
if (this.app.workspace.layoutReady) {
|
||||
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
|
||||
}
|
||||
|
||||
this.setupAutosaveTimer();
|
||||
}
|
||||
|
||||
private setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
if(this.dirty) {
|
||||
this.dirty = false;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
console.log("save");
|
||||
}
|
||||
}
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
|
||||
if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
@@ -188,6 +210,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
this.dirty = false;
|
||||
this.previousSceneVersion = 0;
|
||||
const reactElement = React.createElement(() => {
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
@@ -267,6 +291,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.justLoaded = false;
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(et);
|
||||
if(sceneVersion != this.previousSceneVersion) {
|
||||
this.previousSceneVersion = sceneVersion;
|
||||
this.dirty=true;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const EXCALIDRAW_FILE_EXTENSION = "excalidraw";
|
||||
export const EXCALIDRAW_FILE_EXTENSION_LEN = EXCALIDRAW_FILE_EXTENSION.length;
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const CODEBLOCK_EXCALIDRAW = "excalidraw";
|
||||
export const MAX_COLORS = 5;
|
||||
|
||||
247
src/main.ts
247
src/main.ts
@@ -11,6 +11,7 @@ import {
|
||||
MarkdownPostProcessorContext,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TAbstractFile,
|
||||
} from 'obsidian';
|
||||
import {
|
||||
BLANK_DRAWING,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
EXCALIDRAW_ICON,
|
||||
ICON_NAME,
|
||||
EXCALIDRAW_FILE_EXTENSION,
|
||||
EXCALIDRAW_FILE_EXTENSION_LEN,
|
||||
CODEBLOCK_EXCALIDRAW,
|
||||
DISK_ICON,
|
||||
DISK_ICON_NAME,
|
||||
@@ -58,11 +60,19 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
private transclusionIndex: TransclusionIndex;
|
||||
private activeExcalidrawView: ExcalidrawView;
|
||||
public lastActiveExcalidrawFilePath: string;
|
||||
/*Excalidraw Sync Begin*/
|
||||
private excalidrawSync: Set<string>;
|
||||
private syncModifyCreate: any;
|
||||
/*Excalidraw Sync Begin*/
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
this.activeExcalidrawView = null;
|
||||
this.lastActiveExcalidrawFilePath = null;
|
||||
/*Excalidraw Sync Begin*/
|
||||
this.excalidrawSync = new Set<string>();
|
||||
this.syncModifyCreate = null;
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
async onload() {
|
||||
@@ -195,6 +205,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.transclusionIndex = new TransclusionIndex(this.app.vault);
|
||||
this.transclusionIndex.initialize();
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu: Menu, file: TFile) => {
|
||||
if (file instanceof TFolder) {
|
||||
@@ -209,67 +222,225 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
})
|
||||
);
|
||||
|
||||
//watch filename change to rename .svg
|
||||
this.app.vault.on('rename',async (file,oldPath) => {
|
||||
this.transclusionIndex.updateTransclusion(oldPath,file.path);
|
||||
if (!(this.settings.keepInSync && file instanceof TFile)) return;
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(normalizePath(oldSVGpath));
|
||||
if(svgFile && svgFile instanceof TFile) {
|
||||
const newSVGpath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
await this.app.vault.rename(svgFile,newSVGpath);
|
||||
}
|
||||
});
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.addEventListeners(this);
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => this.addEventListeners(this)));
|
||||
}
|
||||
}
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
public initiateSync() {
|
||||
if(!this.syncModifyCreate) return;
|
||||
const files = this.app.vault.getFiles();
|
||||
(files || [])
|
||||
.filter((f:TFile) => (f.path.startsWith(this.settings.syncFolder) && f.extension == "md"))
|
||||
.forEach((f)=>this.syncModifyCreate(f));
|
||||
(files || [])
|
||||
.filter((f:TFile) => (!f.path.startsWith(this.settings.syncFolder) && f.extension == EXCALIDRAW_FILE_EXTENSION))
|
||||
.forEach((f)=>this.syncModifyCreate(f));
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
//watch file delete and delete corresponding .svg
|
||||
this.app.vault.on('delete',async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
private async addEventListeners(plugin: ExcalidrawPlugin) {
|
||||
const closeDrawing = async (filePath:string) => {
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
if((leaves[i].view as ExcalidrawView).file.path == file.path) {
|
||||
//(leaves[i].view as ExcalidrawView).clear();
|
||||
leaves[i].setViewState({
|
||||
if((leaves[i].view as ExcalidrawView).file.path == filePath) {
|
||||
await leaves[i].setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: {file: null}}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
const reloadDrawing = async (oldPath:string, newPath: string) => {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(newPath);
|
||||
if(!(file && file instanceof TFile)) return;
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
if((leaves[i].view as ExcalidrawView).file.path == oldPath) {
|
||||
(leaves[i].view as ExcalidrawView).setViewData(await plugin.app.vault.read(file),false);
|
||||
plugin.triggerEmbedUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createPathIfNotThere = async (path:string) => {
|
||||
const folderArray = path.split("/");
|
||||
folderArray.pop();
|
||||
const folderPath = folderArray.join("/");
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if(!folder)
|
||||
await plugin.app.vault.createFolder(folderPath);
|
||||
}
|
||||
|
||||
const getSyncFilepath = (excalidrawPath:string):string => {
|
||||
return normalizePath(plugin.settings.syncFolder)+'/'+excalidrawPath.slice(0,excalidrawPath.length-EXCALIDRAW_FILE_EXTENSION_LEN)+"md";
|
||||
}
|
||||
|
||||
const getExcalidrawFilepath = (syncFilePath:string):string => {
|
||||
const syncFolder = normalizePath(plugin.settings.syncFolder)+'/';
|
||||
const normalFilePath = syncFilePath.slice(syncFolder.length);
|
||||
return normalFilePath.slice(0,normalFilePath.length-2)+EXCALIDRAW_FILE_EXTENSION; //2=="md".length
|
||||
}
|
||||
|
||||
const syncCopy = async (source:TFile, targetPath: string) => {
|
||||
await createPathIfNotThere(targetPath);
|
||||
const target = plugin.app.vault.getAbstractFileByPath(targetPath);
|
||||
plugin.excalidrawSync.add(targetPath);
|
||||
if(target && target instanceof TFile) {
|
||||
await plugin.app.vault.modify(target,await plugin.app.vault.read(source));
|
||||
} else {
|
||||
await plugin.app.vault.copy(source,targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
const syncModifyCreate = async (file:TAbstractFile) => {
|
||||
if(!(file instanceof TFile)) return;
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
return;
|
||||
}
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncFilePath = getSyncFilepath(file.path);
|
||||
await syncCopy(file,syncFilePath);
|
||||
break;
|
||||
case 'md':
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawNewPath = getExcalidrawFilepath(file.path);
|
||||
await syncCopy(file,excalidrawNewPath);
|
||||
reloadDrawing(excalidrawNewPath,excalidrawNewPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.syncModifyCreate = syncModifyCreate;
|
||||
|
||||
plugin.app.vault.on('create', syncModifyCreate);
|
||||
plugin.app.vault.on('modify', syncModifyCreate);
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
//watch filename change to rename .svg
|
||||
plugin.app.vault.on('rename',async (file,oldPath) => {
|
||||
plugin.transclusionIndex.updateTransclusion(oldPath,file.path);
|
||||
if(!(file instanceof TFile)) return;
|
||||
/*Excalidraw Sync Begin*/
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
} else {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncOldPath = getSyncFilepath(oldPath);
|
||||
const syncNewPath = getSyncFilepath(file.path);
|
||||
const oldFile = plugin.app.vault.getAbstractFileByPath(syncOldPath);
|
||||
if(oldFile && oldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(syncNewPath);
|
||||
await createPathIfNotThere(syncNewPath);
|
||||
await plugin.app.vault.rename(oldFile,syncNewPath);
|
||||
} else {
|
||||
await syncCopy(file,syncNewPath);
|
||||
}
|
||||
break;
|
||||
case 'md':
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawOldPath = getExcalidrawFilepath(oldPath);
|
||||
const excalidrawNewPath = getExcalidrawFilepath(file.path);
|
||||
const excalidrawOldFile = plugin.app.vault.getAbstractFileByPath(excalidrawOldPath);
|
||||
if(excalidrawOldFile && excalidrawOldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(excalidrawNewPath);
|
||||
await createPathIfNotThere(excalidrawNewPath);
|
||||
await plugin.app.vault.rename(excalidrawOldFile,excalidrawNewPath);
|
||||
} else {
|
||||
await syncCopy(file,excalidrawNewPath);
|
||||
}
|
||||
reloadDrawing(excalidrawOldFile.path,excalidrawNewPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
if (!plugin.settings.keepInSync) return;
|
||||
const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(oldSVGpath));
|
||||
if(svgFile && svgFile instanceof TFile) {
|
||||
const newSVGpath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
await plugin.app.vault.rename(svgFile,newSVGpath);
|
||||
}
|
||||
});
|
||||
|
||||
//watch file delete and delete corresponding .svg
|
||||
plugin.app.vault.on('delete',async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
/*Excalidraw Sync Begin*/
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
} else {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncFilePath = getSyncFilepath(file.path);
|
||||
const oldFile = plugin.app.vault.getAbstractFileByPath(syncFilePath);
|
||||
if(oldFile && oldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(oldFile.path);
|
||||
plugin.app.vault.delete(oldFile);
|
||||
}
|
||||
break;
|
||||
case "md":
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawPath = getExcalidrawFilepath(file.path);
|
||||
const excalidrawFile = plugin.app.vault.getAbstractFileByPath(excalidrawPath);
|
||||
if(excalidrawFile && excalidrawFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(excalidrawFile.path);
|
||||
await closeDrawing(excalidrawFile.path);
|
||||
plugin.app.vault.delete(excalidrawFile);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
closeDrawing(file.path);
|
||||
|
||||
if (this.settings.keepInSync) {
|
||||
if (plugin.settings.keepInSync) {
|
||||
const svgPath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const svgFile = this.app.vault.getAbstractFileByPath(normalizePath(svgPath));
|
||||
const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(svgPath));
|
||||
if(svgFile && svgFile instanceof TFile) {
|
||||
await this.app.vault.delete(svgFile);
|
||||
await plugin.app.vault.delete(svgFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//save open drawings when user quits the application
|
||||
this.app.workspace.on('quit',(tasks) => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
plugin.app.workspace.on('quit',(tasks) => {
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
}
|
||||
});
|
||||
|
||||
//save Excalidraw leaf and update embeds when switching to another leaf
|
||||
this.app.workspace.on('active-leaf-change',(leaf:WorkspaceLeaf) => {
|
||||
if(this.activeExcalidrawView) {
|
||||
this.activeExcalidrawView.save();
|
||||
this.triggerEmbedUpdates();
|
||||
plugin.app.workspace.on('active-leaf-change',(leaf:WorkspaceLeaf) => {
|
||||
if(plugin.activeExcalidrawView) {
|
||||
plugin.activeExcalidrawView.save();
|
||||
plugin.triggerEmbedUpdates();
|
||||
}
|
||||
this.activeExcalidrawView = (leaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) ? leaf.view as ExcalidrawView : null;
|
||||
if(this.activeExcalidrawView)
|
||||
this.lastActiveExcalidrawFilePath = this.activeExcalidrawView.file.path;
|
||||
plugin.activeExcalidrawView = (leaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) ? leaf.view as ExcalidrawView : null;
|
||||
if(plugin.activeExcalidrawView)
|
||||
plugin.lastActiveExcalidrawFilePath = plugin.activeExcalidrawView.file.path;
|
||||
});
|
||||
|
||||
this.transclusionIndex = new TransclusionIndex(this.app.vault);
|
||||
this.transclusionIndex.initialize();
|
||||
}
|
||||
|
||||
|
||||
onunload() {
|
||||
destroyExcalidrawAutomate();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ export interface ExcalidrawSettings {
|
||||
autoexportPNG: boolean,
|
||||
keepInSync: boolean,
|
||||
library: string,
|
||||
/*Excalidraw Sync Begin*/
|
||||
syncFolder: string,
|
||||
excalidrawSync: boolean,
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
@@ -28,6 +32,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
autoexportPNG: false,
|
||||
keepInSync: false,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
/*Excalidraw Sync Begin*/
|
||||
syncFolder: 'excalidraw_sync',
|
||||
excalidrawSync: false,
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -140,5 +148,43 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
this.containerEl.createEl('h1', {text: 'Excalidraw sync'});
|
||||
this.containerEl.createEl('h3', {text: 'This is a hack and a temporary workaround. Turn it on only if you are comfortable with hacky solutions...'});
|
||||
this.containerEl.createEl('p', {text: 'By enabling this feature Excalidraw will sync drawings to a sync folder where drawings are stored in an ".md" file. ' +
|
||||
'This will allow Obsidian sync to synchronize Excalidraw drawings as well... ' +
|
||||
'Whenever your drawing changes, the corresponding file in the sync folder will also get updated. Similarly, whenever a file is synchronized to the sync folder ' +
|
||||
'by Obsidian sync, Excalidraw will sync it with the .excalidraw file in your vault.'});
|
||||
this.containerEl.createEl('p', {text: 'Because this is a temporary workaround until Obsidian sync is ready, I didn\'t implement extensive application logic to manage sync. ' +
|
||||
'Sync might get confused requiring some manual intervention.'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw sync folder')
|
||||
.setDesc('Configure the folder first, before activating the feature! ' +
|
||||
'This is the root folder for your mirrored excalidraw drawings. ' +
|
||||
'Don\'t save other files here, as my algorithm is not prepared to handle those... and I can\'t predict the outcome. ')
|
||||
.addText(text => text
|
||||
.setPlaceholder('.excalidraw_sync')
|
||||
.setValue(this.plugin.settings.syncFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw sync')
|
||||
.setDesc('Enable Excalidraw Sync')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.excalidrawSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.excalidrawSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.initiateSync();
|
||||
}));
|
||||
|
||||
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"1.0.9": "0.11.13",
|
||||
"1.0.8": "0.11.13",
|
||||
"1.0.7": "0.11.13",
|
||||
"1.0.6": "0.11.13",
|
||||
|
||||
Reference in New Issue
Block a user