Compare commits

...

320 Commits

Author SHA1 Message Date
zsviczian
eee264918e 2.4.0-rc-1 2024-08-27 21:43:21 +02:00
zsviczian
89172a88f1 fix android preview render bug 2024-08-27 08:56:57 +02:00
zsviczian
ffdb054291 2.4.0-beta-10 2024-08-26 22:40:45 +02:00
zsviczian
200d39c408 Merge pull request #1960 from dmscode/master
Update zh-cn.ts and add Readme.zh-cn
2024-08-26 22:27:07 +02:00
zsviczian
4306574ace Update Excalidraw Writing Machine 2024-08-26 15:04:58 +02:00
zsviczian
12e3b90458 support wikilinks 2024-08-26 15:04:05 +02:00
dmscode
03364b5d2e Readme.zh-cn 2024-08-26 15:43:28 +08:00
dmscode
4e268991dc Update zh-cn.ts to 2.4.0-beta-9 2024-08-26 14:32:46 +08:00
zsviczian
429c84f940 updated rollup, tsconfig, rollup versions in package.json 2024-08-25 16:53:15 +02:00
zsviczian
e890e4489b replace json.stringify with proper processing, fix small issues with Ephemral state, added worker (inactive) 2024-08-25 16:08:12 +02:00
zsviczian
8466c42217 update Excalidraw package -42 is corrupted 2024-08-24 20:30:51 +02:00
zsviczian
353732f597 2.4.0-beta-9 2024-08-24 13:59:49 +02:00
zsviczian
5599d2507f update writing machine data 2024-08-23 12:43:27 +02:00
zsviczian
70cf6ffe70 Support embeddables 2024-08-23 12:41:33 +02:00
zsviczian
61c9277097 writing machine 2024-08-22 21:59:06 +02:00
zsviczian
401052efd3 svg icon take 2 2024-08-22 20:36:00 +02:00
zsviczian
a57a0e797d update writing machine svg 2024-08-22 20:33:52 +02:00
zsviczian
8f48853e2c updated writing machine script 2024-08-22 17:58:42 +02:00
zsviczian
a62148dc07 publish Excalidraw Writing Machine 2024-08-21 21:53:59 +02:00
zsviczian
3ae890bd86 2.4.0-beta-8 2024-08-19 15:57:23 +02:00
zsviczian
65a4cd4ba5 Merge pull request #1939 from dmscode/master
Update zh-cn.ts 2.4.0-beta-7
2024-08-19 15:38:56 +02:00
dmscode
f63b473bc1 Update zh-cn.ts to the commit "fixed getTemplate, migrade gridSize, gridStep" 2024-08-19 09:42:37 +08:00
稻米鼠
859a5ba03a Merge branch 'zsviczian:master' into master 2024-08-19 09:37:07 +08:00
zsviczian
832b97b179 fixed getTemplate, migrade gridSize, gridStep, 2024-08-18 17:15:03 +02:00
dmscode
e98d688d36 Update zh-cn.ts 2.4.0-beta-7 2024-08-16 15:53:39 +08:00
zsviczian
39318337fe updated set stroke width script 2024-08-16 08:13:55 +02:00
zsviczian
f21215be84 2.4.0-beta-7 2024-08-15 17:45:49 +02:00
zsviczian
0690525af8 Merge pull request #1937 from dmscode/master
Update zh-cn.ts to 2.4.0-beta-6
2024-08-15 17:41:32 +02:00
dmscode
b3176425c5 Merge branch 'master' of https://github.com/dmscode/obsidian-excalidraw-plugin 2024-08-15 18:56:30 +08:00
dmscode
9f2c18b6b6 Update zh-cn.ts to 2.4.0-beta-6 2024-08-15 18:56:20 +08:00
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
zsviczian
bf148adc68 2.4.0-beta-2 2024-08-09 23:36:20 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
zsviczian
9fc0452b70 2.4.0-beta-1 2024-08-08 23:27:57 +02:00
zsviczian
83eda9b3f5 Update README.md 2024-08-04 11:01:12 +02:00
zsviczian
9bfbf47963 2.3.0 2024-08-04 10:37:06 +02:00
zsviczian
252bf411b1 2.3.0-beta-1 2024-08-03 21:26:47 +02:00
zsviczian
5622c019dd rebuild view DEVICE.isDesktop 2024-08-02 17:55:23 +02:00
zsviczian
b32fab7865 2.2.13-1 2024-08-01 22:18:16 +02:00
zsviczian
cafdad1f7a onWindowMigrated 2024-07-30 18:54:54 +02:00
zsviczian
9da40944ab 2.2.13 2024-07-29 18:13:13 +02:00
zsviczian
f678203a64 Merge pull request #1908 from dmscode/master
Update zh-cn.ts to 2.2.11
2024-07-29 17:32:14 +02:00
zsviczian
a9572e08e9 Merge branch 'master' into master 2024-07-29 17:31:58 +02:00
dmscode
ee9b042cdf Update zh-cn.ts to 2.2.11 2024-07-28 20:42:41 +08:00
zsviczian
bc138fa78a fixed storeAction, replaced "none" with "update" #1906 2024-07-28 10:06:31 +02:00
zsviczian
67dbe256f7 2.2.12 2024-07-28 06:55:27 +02:00
zsviczian
8b066d46e2 fix rename and skip inline fonts to cache key 2024-07-28 06:24:30 +02:00
zsviczian
1769a65a82 2.2.11 2024-07-27 21:25:23 +02:00
zsviczian
941eb56769 updated ExcaliAI 2024-07-27 07:09:19 +02:00
zsviczian
a317613ef4 Merge pull request #1859 from Saik0s/patch-1
Update ExcaliAI.md to fix incorrect request json for openai api
2024-07-27 07:05:58 +02:00
zsviczian
173571846f 2.2.10-2 2024-07-26 20:11:20 +02:00
zsviczian
ab1078d393 Fourth Font 2.2.10-1 2024-07-25 21:59:42 +02:00
zsviczian
681321a595 2.2.10-1 fontpicker beta 2024-07-25 21:52:47 +02:00
zsviczian
fcd50d4bc2 Merge pull request #1897 from dmscode/master
Updated zh-cn.ts to 2.2.10
2024-07-23 17:49:00 +02:00
dmscode
7e214e5aaa Updated zh-cn.ts to 2.2.10 2024-07-23 09:54:43 +08:00
zsviczian
de39053857 2.2.10 2024-07-22 22:01:36 +02:00
zsviczian
f543e3218e clippedframe, framesettings 2024-07-21 17:54:00 +02:00
zsviczian
e668aea214 Merge pull request #1890 from dmscode/master
Fixed link path
2024-07-20 08:33:09 +02:00
zsviczian
d810daa735 bump excalidraw version 2024-07-19 12:20:21 +02:00
zsviczian
b94b3118eb improved error message when getPNG fails 2024-07-19 12:19:48 +02:00
dmscode
06cbd0c92d Fixed link path 2024-07-19 12:44:50 +08:00
zsviczian
96ebbbf11d tolerate ! for embedded files 2024-07-17 21:05:01 +02:00
zsviczian
fc1467b05b Support for area, group, frame references in image embeds 2024-07-17 20:31:36 +02:00
zsviczian
53c27f2a59 2.2.9 2024-07-13 17:34:19 +02:00
zsviczian
db80f5c715 allows opacity 0% 2024-07-09 16:45:06 +02:00
zsviczian
405c98ca50 2.2.8 2024-07-05 07:00:42 +02:00
zsviczian
4892aed9e6 Merge pull request #1825 from dmscode/master
Updated and formated zh-cn.ts
2024-07-04 20:33:30 +02:00
zsviczian
1b04b94db2 Merge pull request #1843 from tobybaratta/patch-1
Documentation: Update Custom Pens documentation links
2024-07-04 20:33:01 +02:00
zsviczian
4a430f5fe7 pre-2.2.8 2024-07-04 20:15:33 +02:00
zsviczian
325bfd825f 2.2.7-5 beta 2024-07-04 09:23:49 +02:00
zsviczian
471553f913 2.2.7-4 2024-07-03 00:46:55 +02:00
zsviczian
a43d0689d8 2.2.7-3 without slob 2024-07-01 20:57:47 +02:00
zsviczian
afacb8a94b 2.2.7-2 2024-07-01 20:51:01 +02:00
zsviczian
afe5300e00 2.2.7-1 beta release after refactoring 2024-06-30 23:19:12 +02:00
zsviczian
ef85d0e323 select similar elements 2024-06-30 11:37:22 +02:00
Igor Tarasenko
d3aeedb9f6 Update ExcaliAI.md to fix incorrect request json for openai api 2024-06-28 21:03:58 +02:00
Toby Baratta
7718f69269 Fixing links to file names with spaces
commonmark spec thank you for working with spaces now
2024-06-20 19:17:00 -07:00
Toby Baratta
af50b0c5c6 Update Custom Pens documentation links
**Problem:**
- The README right now contains a broken link to a not-on-main file documenting how to add custom pens. (That document was archived -- I found the original via searching and tried to find the corresponding PRs for the details on the replacement functionality but couldn't find that PR.) 
- Pens are now handled in settings with a different YouTube video showing how to set them up (I think). 
- Adds links to the Hardware Eraser functionality & Auto Draw for Pen. 

Question:
- I think it might be useful to link to the freestyle docs here still, or have some sort of master "Alternative Pens" link that still shows how to do the JSON setup? Not sure if necessary.
2024-06-20 18:55:35 -07:00
dmscode
b576faad82 Updated zh-cn.ts to 2.2.7 2024-06-21 09:34:15 +08:00
稻米鼠
4583e603e9 Merge branch 'zsviczian:master' into master 2024-06-21 09:24:12 +08:00
zsviczian
7ee316a605 2.2.7 2024-06-19 20:56:34 +02:00
dmscode
7dca225691 Updated and formated zh-cn.ts
- Updated to 2.2.6
- Some punctuation to Chinese punctuation
- Add space between Chinese and English
- Change keyname to just first letter upper
- Remove space at line end
2024-06-13 08:23:22 +08:00
zsviczian
8a27d0240a 2.2.6 2024-06-12 22:23:32 +02:00
zsviczian
939bd6fd91 Merge pull request #1815 from dmscode/master
Update zh-cn.ts to 2.2.4
2024-06-12 22:20:20 +02:00
zsviczian
dfbd385de7 2.2.5 2024-06-09 16:34:48 +02:00
dmscode
0d791070dd Update zh-cn.ts to 2.2.4 2024-06-05 19:02:20 +08:00
zsviczian
94fbac38bf 2.2.4 2024-06-02 12:28:52 +02:00
zsviczian
73cf8e75d3 Merge pull request #1679 from evolutioned/patch-1
Update templater_mindmap.md grammar
2024-06-02 06:44:32 +02:00
zsviczian
395cbc104c Merge pull request #1805 from jmhammond/master
Remove autosaveInterval variable but preserve functionality
2024-06-02 06:43:31 +02:00
zsviczian
9a24db8379 Merge pull request #1810 from dmscode/master
Update zh-cn language to 2.2.3
2024-06-02 06:40:29 +02:00
zsviczian
934a5f4838 this to self 2024-06-02 06:39:55 +02:00
dmscode
9022500087 Update zh-cn language to 2.2.3 2024-06-01 12:03:23 +08:00
John Hammond
795807b6cf Remove autosaveInterval variable but preserve functionality 2024-05-29 17:15:14 -05:00
zsviczian
9eff79733c 2.2.3 2024-05-26 22:34:59 +02:00
zsviczian
c4e95d9207 replace commitToHistory with storeAction 2024-05-22 21:22:39 +02:00
zsviczian
32cd3a62b6 DEGUGGING in dev build with process.env.NODE_ENV, + this.lastSceneSnapshot 2024-05-22 20:08:49 +02:00
zsviczian
d05ccc0055 DEBUGGER improved 2024-05-20 19:28:33 +02:00
zsviczian
ff1d7b44b4 2.2.2 2024-05-20 12:25:53 +02:00
zsviczian
2b86ba2128 2.2.1 2024-05-20 09:37:14 +02:00
zsviczian
44a3b30e3b 2.2.0 2024-05-19 14:43:00 +02:00
zsviczian
bb9389c7dd cropping, fold everything, drag and drop fixes 2024-05-18 21:57:54 +02:00
zsviczian
ba4bfe9de7 embeddable theme 2024-05-18 14:33:25 +02:00
zsviczian
3a73b14ebb fixed select similar elements script 2024-05-18 09:35:42 +02:00
zsviczian
31f54db433 removed wrapAt, changed to refreshTextDimensions, updated onBeforeTextSubmit and Edit, replaced all != with !== plus additional type checks 2024-05-17 21:41:37 +02:00
zsviczian
26812dd297 force to open markdown command palette action, fixed decompress 2024-05-15 22:02:06 +02:00
zsviczian
ae4f4b4f08 2.1.8.1-beta-1 2024-05-14 21:07:42 +02:00
zsviczian
4ac0a4c565 2.1.8 2024-05-13 22:49:32 +02:00
zsviczian
69c9f824a0 Update README.md 2024-05-06 21:24:57 +02:00
zsviczian
6a2220c960 updated slideshow timestamp 2024-05-05 19:16:59 +02:00
zsviczian
aa501c2843 2.1.7 2024-05-05 19:13:30 +02:00
zsviczian
efce44f0a7 2.1.6.1-beta-1 2024-05-01 10:57:50 +02:00
zsviczian
491eb83d35 convert app.isMobile to DEVICE.isMobile, fix CropImage angle!==0 2024-04-30 19:31:00 +02:00
zsviczian
35cf0802d1 2.1.6 2024-04-23 22:19:49 +02:00
zsviczian
f768548f60 2.1.5 2024-04-22 20:10:21 +02:00
zsviczian
37789f9907 Update issue templates 2024-04-20 15:21:49 +02:00
zsviczian
131294464e 2.1.4 2024-04-13 09:44:15 +02:00
zsviczian
b7652a41f8 Update directory-info.json 2024-04-08 12:39:47 +02:00
zsviczian
91c5f85ec6 Update Deconstruct selected elements into new drawing.md 2024-04-08 12:38:40 +02:00
zsviczian
985983b31d updated slideshow script 2024-04-07 09:45:11 +02:00
zsviczian
9a27e38ce2 2.1.3 2024-04-06 15:52:34 +02:00
zsviczian
0017ed7c92 2.1.2 2024-04-06 13:32:56 +02:00
evolutioned
d5cf4ace21 Update templater_mindmap.md
Small grammar update
2024-04-02 06:40:55 +11:00
zsviczian
1c899746fd 2.1.1 2024-04-01 20:57:50 +02:00
zsviczian
9ea09c0fdd fixed excalidraw to PDF 2024-04-01 08:06:47 +02:00
zsviczian
cda674c289 2.1.0 2024-03-31 20:29:33 +02:00
zsviczian
ef7d7ccc91 2.0.26 2024-03-30 17:42:59 +01:00
zsviczian
21aa5eb2d6 wordsimthing... 2024-03-24 13:03:18 +01:00
zsviczian
e725fb9b65 improved description 2024-03-24 13:00:26 +01:00
zsviczian
fbd634bfce updated slideshow script 2024-03-24 12:53:53 +01:00
zsviczian
9a686f3827 corrected spelling in messages 2024-03-17 18:14:52 +01:00
zsviczian
999219a0c9 2.0.25 2024-03-17 18:01:18 +01:00
zsviczian
65fd370cc5 Revert "Merge pull request #1647 from blampewpew/feat-save-new-drawing-to-current-folder"
This reverts commit fa03968508, reversing
changes made to 78dace32d4.
2024-03-17 17:48:14 +01:00
zsviczian
232f0c38fa updated deconstruct script 2024-03-17 17:47:30 +01:00
zsviczian
fa03968508 Merge pull request #1647 from blampewpew/feat-save-new-drawing-to-current-folder
FR: Update Excalidraw Folder setting to allow saving drawings in the current folder or subfolder within current folder #1348
2024-03-17 17:14:12 +01:00
zsviczian
78dace32d4 support for templates folder 2024-03-17 16:57:10 +01:00
Bryan
a21e7aa0c5 Update helper text. 2024-03-16 15:13:57 -06:00
Bryan
9b7d828209 Set folder to current active folder if Excalidraw Folder name starts with "./" and embedUseExcalidrawFolder is true. 2024-03-16 15:13:45 -06:00
Bryan
a1071426c0 created getCurrentActiveDirectory helper function and updated reference in getAttachmentsFolderAndFilePath() 2024-03-16 15:09:53 -06:00
zsviczian
23baf21ef5 Merge pull request #1640 from zsviczian/custom-zoom
publish custom zoom script
2024-03-14 15:00:51 +01:00
zsviczian
d280b33073 publish custom zoom script 2024-03-14 14:58:39 +01:00
zsviczian
d1ab6bb7a1 2.0.24 2024-03-12 20:05:05 +01:00
zsviczian
146d04ea64 0.2.23 2024-02-25 16:15:32 +01:00
zsviczian
fd46a3f8ac Merge pull request #1602 from GColoy/Fix-Invert-Colors-Script
Fixed Issue with Invert Colors Script
2024-02-25 15:16:14 +01:00
zsviczian
9974ca1e2e 2.0.22 2024-02-21 18:49:19 +01:00
Felix
2c8a733359 Nested Arrays in colorPalette are taken into account 2024-02-20 19:54:30 +01:00
zsviczian
4c9eeb9a61 2.0.21 2024-02-19 19:32:44 +01:00
zsviczian
de23be8c2d Update directory-info.json 2024-02-19 08:03:22 +01:00
zsviczian
bb25a1fde9 Update ExcaliAI.md 2024-02-19 08:01:43 +01:00
zsviczian
9267c27bc5 Merge pull request #1578 from deining/fix-typos
Fix typos
2024-02-17 17:59:00 +01:00
zsviczian
591f073413 added match image fileId to Select Similar Elements 2024-02-12 17:53:04 +01:00
Andreas Deininger
9c28c1ee00 Fix typos 2024-02-05 01:27:36 +01:00
zsviczian
120d41ea2f 2.0.20 2024-02-04 10:46:16 +01:00
zsviczian
80b24a45ad fix getMaximumGroups 2024-02-04 10:36:24 +01:00
zsviczian
e8db9cbff6 2.0.19 2024-02-02 20:34:23 +01:00
zsviczian
5db4f8dd95 updated Crop Vintage Mask to ensure frame is at top 2024-01-29 22:55:27 +01:00
zsviczian
820b2ff6c4 2.0.18 2024-01-21 16:49:34 +01:00
zsviczian
596102fb68 publish crop vintage 2024-01-21 16:11:19 +01:00
zsviczian
fa5c63b224 publish crop script 2024-01-21 16:08:12 +01:00
zsviczian
aa66b8f716 Crop Vintage Mask Script 2024-01-21 15:41:57 +01:00
zsviczian
c06cdfcfa0 frogg image 2024-01-21 15:39:27 +01:00
zsviczian
e6943cf7f5 Merge pull request #1559 from BenMoon/patch-1
Fix typos
2024-01-21 11:44:26 +01:00
zsviczian
b3a2f067ab Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2024-01-21 11:43:28 +01:00
zsviczian
35407cf13d crop vintage mask image 2024-01-21 11:43:17 +01:00
BenMoon
d671faf712 Fix typos 2024-01-17 09:31:50 +01:00
zsviczian
815ad559ac updated deconstruct selected element into new drawing script 2024-01-16 15:06:12 +01:00
zsviczian
d2cf594756 Update Deconstruct selected elements into new drawing.md 2024-01-16 15:04:30 +01:00
zsviczian
6709f6cf87 updated text arch and split text by lines 2024-01-13 16:38:01 +01:00
zsviczian
f463f79222 updated split text by lines script 2024-01-11 21:15:25 +01:00
zsviczian
51cf3a9219 2.0.17 2024-01-10 22:12:02 +01:00
zsviczian
8700405af8 Merge pull request #1548 from tswwe/patch-4
Update zh-cn.ts
2024-01-10 20:51:15 +01:00
thxnder
d1d082b4f9 Update zh-cn.ts
keep up with en.ts
2024-01-10 20:20:04 +08:00
zsviczian
6dd9d1a056 2.0.16 2024-01-07 21:22:56 +01:00
zsviczian
46b03725e9 2.0.15 2024-01-07 10:57:53 +01:00
zsviczian
65d6577b28 Update README.md 2024-01-03 21:35:01 +01:00
zsviczian
97967f5b70 2.0.14 2024-01-03 16:32:07 +01:00
zsviczian
9323e1fad4 minor formatting change in rollup.config.json 2024-01-03 09:51:04 +01:00
zsviczian
3e4e741b54 2.0.13 2023-12-23 15:28:54 +01:00
zsviczian
4e2d8374e6 2.0.12 2023-12-23 11:02:57 +01:00
zsviczian
2b3037402a 2.0.11 2023-12-21 20:51:21 +01:00
zsviczian
bc67c27a82 updated ExcaliAI script 2023-12-21 19:49:15 +01:00
zsviczian
ffb8f6f00f css rework 2023-12-19 13:52:27 +01:00
zsviczian
d179dfe703 sliding panes support disabled 2023-12-18 17:26:28 +01:00
zsviczian
5701020901 Golden Ratio updated 2023-12-17 12:54:53 +01:00
zsviczian
1f2d795b58 stroke color currentColor GoldenRatio.svg 2023-12-17 09:36:54 +01:00
zsviczian
c123b3ef51 golden ratio svg 2023-12-17 07:57:13 +01:00
zsviczian
d565c7e9e9 Golden Ratio script 2023-12-17 07:54:15 +01:00
zsviczian
710a40c36b golden ratio image 2023-12-17 07:48:25 +01:00
zsviczian
64f9a5dd7d 2.0.10 2023-12-10 21:58:04 +01:00
zsviczian
323fe33c2f 2.0.9 2023-12-10 12:35:10 +01:00
zsviczian
2470f6f531 2.0.8 2023-12-08 17:19:16 +01:00
zsviczian
42968d8299 publishing relative font size cycle 2023-12-07 19:24:31 +01:00
zsviczian
7a50b6c77e Merge pull request #1425 from soraliu/master
feat(scripts): support repeat texts
2023-12-07 18:18:46 +01:00
zsviczian
0a10d5fbc9 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-12-06 18:14:47 +01:00
zsviczian
40d2345501 LaTeX Cleanup 2023-12-06 18:14:44 +01:00
zsviczian
ab97ae5ebc updated modifier key combinations table 2023-12-04 17:25:10 +01:00
zsviczian
7c93e90fc0 2.0.7 2023-12-04 14:49:33 +01:00
zsviczian
d44cf3306b 2.0.6 manifest 2023-12-04 08:44:30 +01:00
zsviczian
324609999f rollback 2023-12-03 23:35:03 +01:00
zsviczian
3f54b851ae updated grid selected images 2023-12-03 20:17:22 +01:00
zsviczian
8bbb04b421 Merge pull request #1465 from 7flash/patch-6
Update Grid Selected Images.md
2023-12-03 20:13:04 +01:00
zsviczian
dc396c8707 fix file utils var 2023-12-03 20:11:22 +01:00
zsviczian
52cc5d3aa7 2.0.5 2023-12-03 20:00:47 +01:00
zsviczian
87b6335905 cleaned out React.createElement 2023-12-01 20:45:37 +01:00
zsviczian
17358f16c8 fix getFileFromURL, remove observers when not needed 2023-12-01 17:21:41 +01:00
zsviczian
23eb268031 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-29 17:28:12 +01:00
zsviczian
27f4cb248d better error handling in ExcaliAI 2023-11-29 17:26:07 +01:00
zsviczian
bbaf4f7a34 updated ExcaliAI and GPT-Draw-a-UI timestamps 2023-11-26 19:59:39 +01:00
zsviczian
559455bf5b fix element.src to element.link 2023-11-26 19:57:42 +01:00
zsviczian
bd519aff08 fixed element.src to element.link 2023-11-26 19:57:04 +01:00
zsviczian
febeb787b5 Address undefined after installing the new version 2023-11-26 18:51:22 +01:00
zsviczian
9f8a9bfa8a 2.0.4 2023-11-26 18:38:31 +01:00
zsviczian
8b1daed0ef excaliAI 2023-11-26 16:06:55 +01:00
zsviczian
44c828c7e7 corrected minor error 2023-11-25 07:06:14 +01:00
Igor Berlenko
afabeaa2f3 Update Grid Selected Images.md 2023-11-24 22:41:51 +08:00
zsviczian
e72c1676c2 2.0.3 2023-11-21 22:51:56 +01:00
zsviczian
5a17eb7054 updated generator 2023-11-21 06:05:19 +01:00
zsviczian
75d52c07b8 lint 2023-11-20 23:05:55 +01:00
zsviczian
4dc6c17486 updated script 2023-11-20 22:32:13 +01:00
zsviczian
e780930799 draw a UI 2023-11-20 21:27:40 +01:00
zsviczian
49cd6a36a1 draw-a-ui-image 2023-11-20 21:20:12 +01:00
zsviczian
d4830983e2 2.0.2 2023-11-19 17:41:41 +01:00
zsviczian
a69fefffdc beta-2 2023-11-19 08:42:41 +01:00
zsviczian
1d0466dae7 2.0.1-beta-1 2023-11-18 17:43:47 +01:00
zsviczian
6e5a853d0f 2.0.2-beta-1 2023-11-18 17:40:59 +01:00
zsviczian
0c702ddf7b Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-18 17:16:26 +01:00
zsviczian
fdbffce1f9 Embeddable Settings 2023-11-18 17:16:24 +01:00
zsviczian
2872b4e3ce Merge pull request #1441 from heinrich26/patch-1
Fixed some typos
2023-11-18 17:14:45 +01:00
Hendrik Horstmann
0ba55e51e9 Fixed some typos 2023-11-15 10:50:16 +01:00
zsviczian
5887bf377d 2.0.1 2023-11-14 20:35:47 +01:00
zsviczian
c440dd9cf0 2.0.0 2023-11-13 22:16:43 +01:00
zsviczian
21bc1f7fa6 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-12 17:29:40 +01:00
zsviczian
4279b13554 fix svg export 2023-11-12 17:29:37 +01:00
zsviczian
0e106b7c7b Merge pull request #1429 from 7flash/patch-5
Update Slideshow.md
2023-11-12 10:59:33 +01:00
zsviczian
4d7d1fba3a Image cache initialization watchdog 2023-11-12 09:43:34 +01:00
zsviczian
a35ea5e9da Adding the Assistant font, dynamic styling improvements, frame export colors, Removing ExcalidrawRef 2023-11-12 07:19:09 +01:00
zsviczian
48466f624d 1.9.29 beta 2023-11-11 11:57:23 +01:00
Igor Berlenko
f306b20449 Update Slideshow.md 2023-11-11 18:28:57 +08:00
zsviczian
fc4fd685ba publish support 2023-11-11 07:21:14 +01:00
zsviczian
7449df6ac6 docs 2023-11-09 19:00:03 +01:00
zsviczian
1390333c4c publish EA.d.ts to docs 2023-11-09 18:58:24 +01:00
zsviczian
a9f545a1b2 updated docs 2023-11-09 18:57:08 +01:00
Sora Liu
f80a96c703 feat(scripts): support repeat texts 2023-11-08 17:11:55 +04:00
zsviczian
f291c15bbc restructured render Excalidraw, support for cssclass, context menu 2023-11-06 20:37:53 +01:00
zsviczian
18821e1a67 1.9.28 2023-11-04 14:28:30 +01:00
zsviczian
5dd65d691c mermaid to retain SVG for legacy, LASER-config, PDF import improvements, image link click improvements, hover on image with link, dynamic styling fix, image cache fix 2023-11-03 23:02:09 +01:00
zsviczian
8f96dbc21d 1.9.27 2023-10-28 20:51:55 +02:00
zsviczian
f71623f8a1 1.9.26 2023-10-25 18:55:18 +02:00
zsviczian
b380420cac Update README.md 2023-10-23 07:50:52 +02:00
zsviczian
21cccd4475 updated readme 2023-10-23 07:50:10 +02:00
zsviczian
06475aea78 1.9.25 2023-10-22 21:15:32 +02:00
zsviczian
84af0c2d5c Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-10-15 17:14:41 +02:00
zsviczian
84648f5a56 1.9.24 2023-10-15 17:14:39 +02:00
zsviczian
60b7988860 fixed keyboard event handler when switching back and forth between views 2023-10-12 22:12:26 +02:00
zsviczian
732b7ad424 updated the slideshow script with the laserpointer 2023-10-07 21:44:06 +02:00
zsviczian
a664c34418 bump excalidraw package 2023-10-07 18:56:04 +02:00
zsviczian
eaf3b7f7d7 1.9.23 2023-10-07 18:47:33 +02:00
zsviczian
03edbaf545 1.9.22 2023-10-07 14:22:07 +02:00
zsviczian
c3edf23023 Merge pull request #1354 from LiamSwayne/patch-1
Grammar fix
2023-10-07 14:09:34 +02:00
Liam Swayne
5f765609f9 grammar fix 2023-10-04 13:47:07 -04:00
zsviczian
3c1beac822 added image 2023-10-01 17:55:33 +02:00
zsviczian
f30f66969c concatenate lines script 2023-10-01 17:48:43 +02:00
zsviczian
4e6fb48bf0 1.9.20 2023-09-30 22:27:16 +02:00
zsviczian
53225ba5a7 Merge pull request #1338 from Aeases/master
Fix Excalidraw Pasting with middle mouse button on linux
2023-09-30 18:41:34 +02:00
zsviczian
c04967d775 should render mermaid and disabling ctrl hover preview when object snapping 2023-09-30 18:41:11 +02:00
zsviczian
6bb7a3c0e5 publish boolean operations 2023-09-30 18:31:42 +02:00
zsviczian
2c85191671 mermaid 2 2023-09-29 19:22:22 +02:00
zsviczian
637235eb9d mermaid-2 beta 2023-09-29 19:16:38 +02:00
zsviczian
13e4203c44 Merge pull request #1346 from Raboro/patch-1
fixed typos & syntax errors
2023-09-26 18:49:21 +02:00
Marius Wörfel
d04b628d55 fixed typos & syntax errors 2023-09-25 22:16:57 +02:00
zsviczian
2eab13661d Merge pull request #1341 from GColoy/boolean-Operations
Boolean operations
2023-09-24 16:32:39 +02:00
GColoy
6eefb49eb0 Added version requirement to Boolean operations 2023-09-23 01:14:25 +02:00
GColoy
6878b1f969 added description and index entries 2023-09-23 01:07:58 +02:00
GColoy
0ba784564f replaced ShadowClone Propertie with group id 2023-09-23 00:07:33 +02:00
GColoy
62804cdc63 Boolean Operations Icon 2023-09-22 21:56:49 +02:00
GColoy
ff900b4b61 account for unclosed Lines 2023-09-22 21:56:34 +02:00
GColoy
e34f03c0d1 updated boolean Operations for new api 2023-09-20 21:25:40 +02:00
Felix
2a9cf1cf6b Merge branch 'zsviczian:master' into boolean-Operations 2023-09-20 21:24:09 +02:00
GColoy
d4159dbe75 Corrected Polybool import 2023-09-20 21:23:28 +02:00
GColoy
7cd68304c3 added Boolean Operations.md 2023-09-19 19:00:20 +02:00
GColoy
0d7a76310c added polybool library 2023-09-16 21:06:34 +02:00
zsviczian
056be574ef mermaid beta 2023-09-16 14:30:49 +02:00
zsviczian
f6e2886185 added basic mermaid support 2023-09-16 14:24:04 +02:00
Aeases
7b178ce2c8 Fix Excalidraw Pasting with middle mouse button on linux 2023-09-16 20:02:16 +08:00
zsviczian
e4d05ac284 added otf, fixed replaceSVGColors null 2023-09-12 18:28:14 +02:00
zsviczian
5311f53612 Merge pull request #1318 from tswwe/patch-3
Update zh-cn.ts
2023-09-09 09:58:10 +02:00
thxnder
f20ce396f0 Update zh-cn.ts
Keep up with en.ts
2023-09-09 12:26:23 +08:00
zsviczian
996f1f79f1 update index-new with deconstruct video link 2023-09-03 11:30:28 +02:00
zsviczian
cd58d1af06 1.9.19 2023-09-03 11:27:04 +02:00
zsviczian
9d9201b4d1 text aura 2023-09-03 11:03:24 +02:00
zsviczian
684b2f6268 text aura image 2023-09-03 10:02:48 +02:00
zsviczian
de08f4584d silent create - EA 2023-09-03 07:46:32 +02:00
zsviczian
a937b5d70a 1.9.18 2023-08-27 22:22:05 +02:00
zsviczian
95b99edede implemented editor-paste event listner 2023-08-27 15:13:45 +02:00
zsviczian
cdf7591e0f publis ellipse script 2023-08-27 13:03:52 +02:00
zsviczian
f373867356 Merge pull request #1293 from GColoy/Split-Ellipse
ea-script: Split ellipse
2023-08-27 13:00:27 +02:00
zsviczian
041efcab74 grid color now sets two colors in appstate 2023-08-27 07:18:11 +02:00
zsviczian
e1fe3eeaab fixed constants.ts filename casing issue. Updated tsconfig and rollout.config 2023-08-26 11:02:00 +02:00
GColoy
3793148c77 added index entries for split ellipse 2023-08-22 01:02:06 +02:00
GColoy
5dbfeb085e added split ellipse script and images 2023-08-22 01:01:50 +02:00
zsviczian
f79181c76a fix embed quote 2023-08-20 07:09:35 +02:00
zsviczian
c0df46cb7b 1.9.17 2023-08-19 20:23:59 +02:00
zsviczian
aa7dcf7604 fix image cache, fix ea error, insert quote 2023-08-16 06:32:51 +02:00
zsviczian
fe4a39afc5 toggle grid 2023-08-15 20:50:33 +02:00
zsviczian
4185192954 toggle grid 2023-08-15 20:41:27 +02:00
zsviczian
3a9ee63c97 Merge pull request #1280 from GColoy/ToggleGrid
Toggle grid ea-script
2023-08-15 20:38:08 +02:00
GColoy
d2d2537867 Added Index entrys 2023-08-15 16:29:19 +02:00
GColoy
97fe819737 added ToggleGrid script 2023-08-15 16:09:51 +02:00
zsviczian
9fdca28579 1.9.16 2023-08-11 17:30:31 +02:00
zsviczian
261e093700 1.9.15 2023-08-10 19:18:24 +02:00
zsviczian
07a651c2c8 update 2023-08-10 18:49:03 +02:00
zsviczian
6c0a1f9a4d index-new update 2023-08-10 18:47:18 +02:00
zsviczian
9d941a4e44 updated index-new.md 2023-08-10 18:45:37 +02:00
zsviczian
cbb8f676af implemented script store search 2023-08-09 19:59:16 +02:00
zsviczian
3bcf460ce4 styles manager improvements 2023-08-08 20:00:37 +02:00
zsviczian
23dd4883e3 semi translucent background for embeddables 2023-08-07 21:43:40 +02:00
zsviczian
ce2e0fd408 stylesManager 2023-08-07 20:31:51 +02:00
158 changed files with 23965 additions and 6432 deletions

View File

@@ -1,12 +1,22 @@
---
name: Bug report
about: Create a report to help me improve Excalidraw
about: When something is clearly broken. Everything else is a feature request.
title: 'BUG: '
labels: ''
assignees: ''
---
Help me help you. I am a one man show doing this plugin as a part time hobby. There is no point in flooding me with issues, if there are too many, and they are poorly documented, I will just ignore them. Sorry...
Before creating a bug report, please
1. review recent release notes - maybe there is already an answer,
2. search issues (including closed ones) to see if there is anything similar.
⚠️ I will have to close all recorded bugs that do not provide this background information. Sorry, I need to control my workload/time. ⚠️
--------
**Your environment**
Please run `Command Palette/Show Debug info` in Obsidian and paste the result here.

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ stats.html
hot-reload.bat
data.json
lib
dist
#VSCode
.vscode

2
.nvmrc
View File

@@ -1 +1 @@
18
16

View File

@@ -195,7 +195,7 @@ 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.
This is relevant when using the `addArrow()` and the `connectObjects()` functions.
## canvas
Sets the properties of the canvas.

View File

@@ -1,4 +1,4 @@
The project runs with `node 16.10.0`. Some packages will throw dependency errors if you try to compile with a higher node version.
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:

286
README.md
View File

@@ -1,10 +1,17 @@
# Excalidraw
[简体中文](./README.zh-cn.md)
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
## Video Walkthrough
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
### Here's my complete catalog of videos:
<a href="https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/Catalogue+of+Videos"><img width="380" alt="image" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/2577e5ad-7a21-4c62-acd5-4fe80c8a8a95"></a>
<br>
<details><summary>10 Part (slightly outdated) Video Walkthrough</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 Getting Started</a><br>
@@ -58,55 +65,57 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Eraser, left-handed mode, improved filename configuration</a><br>
</details>
### Beta testing
The plugin follows a monthly release schedule. If you want to receive more frequent updates with new features (e.g. shiny new stuff available on excalidraw.com, but not yet in Obsidian) and minor bug fixes, then join the beta community.
[![Thumbnail - 20240803 Excalidraw Release Approach (Custom)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw Plugin Release Strategy (Phone)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## Features
- The plugin integrates Excalidraw seamlessly into Obsidian including Command Palette actions, File
Explorer features, Option Menu commands, and the Ribbon Button.
- <kbd>CTRL/CMD+Click</kbd> on the ribbon button, or in the file explorer to create / open drawings
in a new pane.
- The plugin integrates Excalidraw seamlessly into Obsidian, including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- <kbd>CTRL/CMD+Click</kbd> on the ribbon button or in the file explorer to create / open drawings in a new pane.
### Settings
Settings will allow you to customize Excalidraw to your needs. The plugin comes with tons of settings. I tried adding meaningful explanations to these settings, so please be patient and look for the setting, for most requests a setting already exists.
Settings will allow you to customize Excalidraw to your needs. The plugin comes with tons of settings. I tried adding meaningful explanations to these settings, so please be patient and look for the setting, for most requests, a setting already exists.
Plugin settings are grouped into the following sections:
- **Basic settings**: such as default folders to use
- **Saving**: compression and autosave timer
- **Filename**: configure the automatically created Excalidraw filename
- **Display**: settings that effect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings)
- **Links and transclusions**: Settings that effect how links and embedded items behave on the Excalidraw canvas
- **Markdown-embed settings**: These settings control how markdown documents from your Vault embedded into Excalidraw drawings will behave
- **Embed & Export**: Settings that control how Excalidraw images are displayed when embedding them into markdown documents
- **Auto-export Settings**: You can configure Excalidraw to create a PNG or SVG copy of your drawing each time it gets saved
- **Compatibility features**: Check these settings if you edit the Excalidraw drawings outside Obsidian (e.g. in LogSeq, Visual Studio, on the web, etc.)
- **Basic settings**: such as default folders to use.
- **Saving**: compression and autosave timer.
- **Filename**: configure the automatically created Excalidraw filename.
- **Display**: settings that effect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings).
- **Links and transclusions**: Settings that effect how links and embedded items behave on the Excalidraw canvas.
- **Markdown-embed settings**: These settings control how markdown documents from your Vault embedded into Excalidraw drawings will behave.
- **Embed & Export**: Settings that control how Excalidraw images are displayed when embedding them into markdown documents.
- **Auto-export Settings**: You can configure Excalidraw to create a PNG or SVG copy of your drawing each time it gets saved.
- **Compatibility features**: Check these settings if you edit the Excalidraw drawings outside Obsidian (e.g. in LogSeq, Visual Studio, on the web, etc.).
- **Experimental features**: There are advanced features that are implemented as "clever" hacks. Features include defining a fourth font, adding a custom icon to distinguish Exalidraw files in the Obsidian file explorer, OCR settings, and more.
- **Settings for installed Scripts**: Some of the scripts you install from the Script Library come with settings. Script settings are installed the first time you run the script. So to access settings for a script, install the script, run it for the first time, then look for the settings in plugin settings.
- **Settings for installed Scripts**: Some of the scripts you install from the Script Library come with settings. Script settings are installed the first time you run the script. So to access settings for a script, install the script, run it for the first time and then look for the settings in plugin settings.
#### Templates
- Template for new drawings. The template will restore stroke properties.
This means you can set up defaults in your template for stroke color, stroke width,
opacity, font family, font size, fill style, stroke style, etc.
This also applies to ExcalidrawAutomate.
- Via the template you can customize the color palette used by Excalidraw.
- Switch to Markdown view.
- Scroll down to the bottom of the file and find `"AppState": {`.
- Find `"customColorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma separated list of valid HTML colors (e.g. `#FF0000` for red)
in the array for each of the variables.
- See my videos above for further help.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- Via the template, you can customize the color palette used by Excalidraw.
- Switch to Markdown view.
- Scroll down to the bottom of the file and find `"AppState": {`.
- Find `"customColorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma-separated list of valid HTML colors (e.g. `#FF0000` for red).
in the array for each of the variables.
- See my videos above for further help.
#### Export
- If portability is important to you:
- Auto-export SVG and/or PNG files including keep-in-sync feature so you can
embed SVG/PNG into your documents instead of embedding excalidraw files.
- You can override export settings for an individual file by adding the `excalidraw-autoexport`
frontmatter key. Valid values for this key are `none`, `both`, `png` and `svg`.
- Auto-export SVG and/or PNG files, including the keep-in-sync feature, so you can
embed SVG/PNG into your documents instead of embedding excalidraw files.
- You can override export settings for an individual file by adding the `excalidraw-autoexport`
frontmatter key. Valid values for this key are `none`, `both`, 'png', and `svg`.
- Specify the default width of embedded drawings.
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy `.excalidraw` files.
@@ -114,137 +123,144 @@ Plugin settings are grouped into the following sections:
- Enable / disable autosave.
### Embedding your drawings into markdown documents
- You can customize the size and position of the embedded images 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](https://www.scaler.com/topics/align-image-in-html/).
- Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element.
- See [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css) for more insight.
- `![[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](https://www.scaler.com/topics/align-image-in-html/).
- Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element.
- See [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css) for more insight.
- Excalidraw drawings do not display in Obsidian Publish. If you want to use Excalidraw in your published documents, you can configure in plugin settings, under `Embed & Export`, to automatically insert a PNG or SVG version of the drawing in your document when creating a new file. See `type of file to insert into document`
- Under `Export settings` you can also configure to automatically export a dark and light version of the image, in case your published site supports dark and light mode.
- Under `Export settings`, you can also configure to automatically export a dark and light version of the image, in case your published site supports dark and light modes.
### Hyperlinks and Drag & Drop support
![](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/images/excalidraw-modifiers.png)
#### Hyperlinks
- Supports hyperlinks e.g.
- `https://zsolt.blog`,
- `[Obsidian](https://obsidian.md)`, and
- internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian
setting Files & Links/Automatically Update Internal Links enabled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section
- you can also specify word wrapping for transcluded text by adding the max character count
in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience you can also use the command palette to insert links into drawings
- <kbd>CTRL/CMD + hover</kbd> to bring up the Obsidian quick preview for the link. (On Mac it is <kbd>CTRL+CMD+hover</kbd>).
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
- Supports hyperlinks, e.g.
- `https://zsolt.blog`,
- `[Obsidian](https://obsidian.md)`, and
- Internal links, e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enabled.
- Links in drawings will show up in the backlinks of documents.
- Transclusions are supported:
- `![[myfile#^blockref]]` will convert the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section.
- You can also specify word wrapping for transcluded text by adding the max character count:
in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience, you can also use the command palette to insert links into drawings.
- <kbd>CTRL/CMD + hover</kbd> to bring up the Obsidian quick preview for the link. (On Mac, it is <kbd>CTRL+CMD+hover</kbd>).
- Using the block reference, you can also reference & transclude text that appears on drawings, in other documents.
#### Drag & Drop support
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw. See table above for the varios modifier key combinations.
- Note: anchoring an image to 100% of its size is a very niche feature with a very particular behavior that I built primarily for myself
- (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉).
- This will reset your embedded image to 100% size every time you open the Excalidraw drawing,
or in case you have embedded an Excalidraw drawing on your canvas inserted using this function,
every time you update the embedded drawing, it will be scaled back to 100% size.
- This means that even if you resize the image on the drawing, it will reset to 100% the next time you open
the file or you modify the original embedded object. This feature is useful when you
decompose a drawing into separate Excalidraw files, but when combined onto a single canvas
you want the individual pieces to maintain their actual sizes. I use this feature to
construct Book-on-a-Page summaries from atomic drawings.
- You can drag files from the Obsidian file explorer, and they will become links to those files in Excalidraw. See the table above for the various modifier key combinations.
- Note: Anchoring an image to 100% of its size is a very niche feature with a very particular behavior that I built primarily for myself.
- (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉).
- This will reset your embedded image to 100% size every time you open the Excalidraw drawing,
or in case you have embedded an Excalidraw drawing on your canvas inserted using this function,
every time you update the embedded drawing, it will be scaled back to 100% size.
- This means that even if you resize the image on the drawing, it will reset to 100% the next time you open
the file, or you modify the original embedded object. This feature is useful when you
decompose a drawing into separate Excalidraw files, but when combined onto a single canvas
you want the individual pieces to maintain their actual sizes. I use this feature to
construct book-on-a-page summaries from atomic drawings.
- You can drag and drop text from Markdown views onto Excalidraw.
- You can drag and drop web addresses from your browser and they will become links.
- You can drag and drop YouTube links and thumbnails and they will be YouTube links with thumbnails in Excalidraw
- You can drag and drop web addresses from your browser, and they will become links.
- You can drag and drop YouTube links and thumbnails, and they will be YouTube links with thumbnails in Excalidraw.
### LaTeX
Insert LaTeX formulas using the Command Palette action "Insert LaTeX formula".
You can edit formulas either in Markdown view, or by <kbd>CTRL/CMD + Click</kbd> on the formula.
You can edit formulas either in Markdown view or by <kbd>CTRL/CMD + Click</kbd> on the formula.
### Image support
- On iOS and Android you can add images from your camera by pressing the add image button in Excalidraw.
- On iOS and Android, you can add images from your camera by pressing the add image button in Excalidraw.
- You can copy/paste images into your drawing. Images will be saved in your vault.
- You can drag and drop images as explained above.
- URL link to images on the web: You can drag images from a webpage to Excalidraw. If you hold down the CTRL button while dropping the image to Excalidraw, the image will not be saved to your vault. Excalidraw will load the image from the URL. Note, that if you do not have internet access, or these images are deleted from the internet, they will also disappear from your drawing.
- If you page an image URL to excalidraw (simply click copy on the url, then click paste on the excalidraw canvas), the image will be inserted with a link to the image on the web. Again, the image won't be save to your vault, only the link.
- If you drop a YouTube video link it will be convereted into a thumbnail photo with an element link pointing to the video.
- URL link to images on the web: You can drag images from a webpage to Excalidraw. If you hold down the CTRL button while dropping the image to Excalidraw, the image will not be saved to your vault. Excalidraw will load the image from the URL. Note that if you do not have internet access or if these images are deleted from the internet, they will also disappear from your drawing.
- If you page an image URL to excalidraw (simply click copy on the url, then click paste on the excalidraw canvas), the image will be inserted with a link to the image on the web. Again, the image won't be saved to your vault, only the link.
- If you drop a YouTube video link, it will be converted into a thumbnail photo with an element link pointing to the video.
### Block referencing parts of images
For more details see this [video](https://youtu.be/yZQoJg2RCKI)
For more details, see this [video](https://youtu.be/yZQoJg2RCKI)
- When referencing an element on the canvas in a link pointing to an Excalidraw file using
- the elementId or the section header (i.e. a Text Element containing the `# <Section title>`)
- e.g. `[[file#^elementID]]`,
- you can add the `group=` prefix,
- e.g. `[[file#^group=elementID]]` or
- the `area=` prefix,
- e.g. `[[file#area=Section heading]]`.
- If the `group=` prefix is found Excalidraw will select the group of elements in the
same group as the element referenced by the elementID (block reference) or the section heading.
- If the `area=` prefix is found Excalidraw will insert a cutout of the image around the referenced element.
- Note that the `area=` selector is not supported when embedding Excalidraw as PNG into your markdown documents.
- Referencing the elementID of a text element without the `group=` or `area=` prefix will
transclude the element as plain text. Referencing a non-Text Element (e.g. rectangle,
ellipse, etc.) without the `group=` or `area=` prefix will result in an Obsidian error
since these elementIds are not present in the Excalidraw markdown file as block
references.
- The elementId or the section header (i.e. a Text Element containing the `# <Section title>`)
- e.g. `[[file#^elementID]]`,
- You can add the `group=` prefix,
- e.g. `[[file#^group=elementID]]` or
- The `area=` prefix,
- e.g. `[[file#area=Section heading]]`.
- If the `group=` prefix is found, Excalidraw will select the group of elements in the
same group as the element referenced by the elementID (block reference) or the section heading.
- If the `area=` prefix is found, Excalidraw will insert a cutout of the image around the referenced element.
- Note that the `area=` selector is not supported when embedding Excalidraw as a PNG into your markdown documents.
- Referencing the elementID of a text element without the `group=` or `area=` prefix will
transclude the element as plain text. Referencing a non-Text Element (e.g. rectangle,
ellipse, etc.) without the `group=` or `area=` prefix will result in an Obsidian error.
since these elementIds are not present in the Excalidraw markdown file as block
references.
### Markdown
- Since 1.2.0 Drawing files are stored in Markdown files
- You can add tags to drawings
- You can add metadata to the YAML front matter of drawings
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
- Excalidraw documents now show in graph view.
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- `excalidraw-default-mode: view|zen` Open this document in view mode or zen mode by defult. Default view mode is excellent for presentation slides.
- Frontmatter tags to customize image export at a file level [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519). If these keys are present they will override the default excalidraw embed and export settings.
- `excalidraw-export-transparent: true`: true == Transparent / false == with background.
- `excalidraw-export-dark`: true == Dark mode / false == light mode.
- `excalidraw-export-padding`: Specify the export padding for the image
- `excalidraw-export-pngscale`: This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.
- Since 1.2.0, drawing files are stored in Markdown files.
- You can add tags to drawings.
- You can add metadata to the YAML front matter of drawings.
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, and it will be preserved as part of the document.
- Excalidraw documents now show up in graph view.
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- `excalidraw-default-mode: view|zen` Open this document in view mode or zen mode by defult. The default view mode is excellent for presentation slides.
- Frontmatter tags to customize image export at a file level [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519). If these keys are present, they will override the default Excalidraw embed and export settings.
- `excalidraw-export-transparent: true`:  true == Transparent / false == with background.
- `excalidraw-export-dark`: true == Dark mode / false == light mode.
- `excalidraw-export-padding`: Specify the export padding for the image.
- `excalidraw-export-pngscale`: This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.
### Embed complete markdown files into your drawings
- Drag from the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</kbd> while dropping the file onto the canvas.
Drag the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</kbd> while dropping the file onto the canvas.
- Use the command palette action: `Insert markdown file from vault`
- Use custom woff, woff2 or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
- You can set a custom css for rendering the snapshot image of your markdown document.
Only operating system standard fonts are supported as the font-family (
[Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list),
[Mac & iOS](https://developer.apple.com/fonts/system-fonts/)
), plus you can set one additional custom font using the setting explained above.
- (for a demonstration watch this [video](https://youtu.be/K6qZkTz8GHs) and check out this
- [sample css](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281)).
- To help with styling you can observe the SVG snapshot of the markdown document created by Excalidraw.
- Open Obsidian Developer Console (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>) and
- execute the following command: `ExcalidrawAutomate.mostRecentMarkdownSVG`
- You can control appearance of the embedded markdown file on a file by file
bases by adding the following front matter keys to your markdown document:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`,
- you can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- Switch to markdown view or use <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> click on the image to edit properties of the embed:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
- Use custom woff, woff2, or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
- You can set a custom CSS for rendering the snapshot image of your markdown document. Only operating system-standard fonts are supported as the font-family ([Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list), [Mac & iOS](https://developer.apple.com/fonts/system-fonts/)), plus you can set one additional custom font using the setting explained above.
- (for a demonstration, watch this [video](https://youtu.be/K6qZkTz8GHs) and check out this
- [sample css](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281)).
- To help with styling, you can observe the SVG snapshot of the markdown document created by Excalidraw.
- Open Obsidian Developer Console (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>) and
- Execute the following command: `ExcalidrawAutomate.mostRecentMarkdownSVG`
- You can control the appearance of the embedded markdown file on a file by file
  bases by adding the following front matter keys to your markdown document:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`,
- You can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- Switch to markdown view or use <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> click on the image to edit the properties of the embed:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
### Custom Font, Custom Pen, OCR support, SVG import
- In plugin settings you can add a custom 4th font. For more details see this [video](https://youtu.be/eKFmrSQhFA4)
- The plugin includes OCR support using Taskbone OCR. For more details see this [video](https://youtu.be/7gu4ETx7zro)
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details see this [video](https://youtu.be/vlC1-iBvIfo)
- You can define custom freedraw pens. See documentation [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
- In plugin settings, you can add a custom fourth font. For more details, see this [video](https://youtu.be/eKFmrSQhFA4)
- The plugin includes OCR support using Taskbone OCR. For more details, see this [video](https://youtu.be/7gu4ETx7zro)
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details, see this [video](https://youtu.be/vlC1-iBvIfo)
- You can define custom pens and higlighters and pin them to the sidebar. For more details, see this [video](https://youtu.be/OjNhjaH2KjI). Using ExcalidrawAutomate, you can add support for [auto-toggling](<ea-scripts/Auto Draw for Pen.md>) pen & support for [hardware eraser buttons](<ea-scripts/Hardware Eraser Support.md>).
### Script Engine
- Since 1.5.0 you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](ea-scripts/README.md).
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See the demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
### Other
- Left-handed mode
- Includes full
- [QuickAdd](https://github.com/chhoumann/quickadd),
@@ -269,7 +285,7 @@ report a bug or request an enhancement.
## Say Thank You
If you are enjoying Excalidraw then please support my work and enthusiasm by buying me a coffee on
If you are enjoying Excalidraw, then please support my work and enthusiasm by buying me a coffee on
[https://ko-fi/zsolt](https://ko-fi.com/zsolt).
Please also help spread the word by sharing about the Obsidian Excalidraw Plugin on Twitter, Reddit,
@@ -284,4 +300,4 @@ You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on m
## Friends of Excalidraw
If you enjoy Excalidraw, consider giving [ExcaliBrain](https://github.com/zsviczian/excalibrain) a try (also available via Obsidian Community Plugins).
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>

285
README.zh-cn.md Normal file
View File

@@ -0,0 +1,285 @@
# Excalidraw
> 此说明当前更新至 2.4.0-beta-9。
Obsidian-Excalidraw 插件将 [Excalidraw](https://excalidraw.com/) 这一功能丰富的草图工具集成到 Obsidian 中。您可以在您的库中存储和编辑 Excalidraw 文件,可以将图形嵌入到文档中,还可以在 Excalidraw 中链接到文档和其他图形。有关 Excalidraw 功能的展示,请查看我的博客文章 [这里](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) 或观看以下视频。
## 视频演示
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
### 这是我完整的视频目录:
<a href="https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/Catalogue+of+Videos"><img width="380" alt="image" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/2577e5ad-7a21-4c62-acd5-4fe80c8a8a95"></a>
<br>
<details><summary>10 部分(稍微过时)视频演示</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 入门</a><br>
<a href="https://youtu.be/Iy_oVTq12Gw" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;2 基本形状和功能</a><br>
<a href="https://youtu.be/QOL1KF7-kdc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;3 元素分组</a><br>
<a href="https://youtu.be/aSgcbfspvfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;4 模板库</a><br>
<a href="https://youtu.be/MaJ5jJwBRWs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;5 嵌入</a><br>
<a href="https://youtu.be/MXzeCOEExNo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;6 链接</a><br>
<a href="https://youtu.be/R0IAg0s-wQE" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;7 Markdown</a><br>
<a href="https://youtu.be/ibdS7ykwpW4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;8 模板</a><br>
<a href="https://youtu.be/VRZVujfVab0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;9 Excalidraw 自动化</a><br>
<a href="https://youtu.be/D1iBYo1_jjc" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;10 杂项</a><br>
</details>
<details><summary>将内容嵌入 Excalidraw</summary>
<a href="https://www.youtube.com/watch?v=_c_0zpBJ4Xc&" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像元素</a><br>
<a href="https://youtu.be/r08wk-58DPk" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732412-1c65227e-4381-406d-847a-b001ab3506ca.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;LaTeX 演示</a><br>
<a href="https://youtu.be/tsecSfnTMow" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143732440-90bfa029-8615-462e-ada3-c903d71a82c9.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入</a><br>
<a href="https://youtu.be/K6qZkTz8GHs" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/143783906-15cee494-c6d5-4495-a2ca-74634e4e7355.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Markdown 嵌入高级功能</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
</details>
<details><summary>脚本引擎商店 - Excalidraw 自动化</summary>
<a href="https://youtu.be/hePJcObHIso" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;介绍脚本引擎</a><br>
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;脚本引擎商店</a><br>
</details>
<details><summary>使用颜色</summary>
<a href="https://youtu.be/6PLGHBH9VZ4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773147-5418a0ab-6be5-4eb0-a8e4-d6af21b1b483.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;颜色 - Excalidraw 基础(自定义)</a><br>
<a href="https://youtu.be/epYNx2FSf2w" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773211-9e871be7-0795-4dc7-947e-c6c275e690d0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 调色板(自定义)</a><br>
<a href="https://youtu.be/Amhlv6r9WvM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773268-400cfb1b-6bde-45e0-9e4b-91bbaa461cf0.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;“艺术”颜色渐变</a><br>
<a href="https://youtu.be/r9oB1SlK1GU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773527-ef35c8b9-1a6d-4415-9c7e-b667fb17535d.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;美丽草图的简单规则</a><br>
<a href="https://youtu.be/7gJDwNgQ6NU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/195988535-a133a9b9-d094-45ba-ba64-c994b9a1e0ef.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;ColorMaster 脚本编写</a><br>
</details>
<details><summary>链接和块引用</summary>
<a href="https://youtu.be/qiKuqMcNWgU" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/171635214-30533c45-94fa-436e-83a9-b2ec99f190e2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接视觉思维的 6 种策略 v4</a><br>
<a href="https://youtu.be/yZQoJg2RCKI" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/185791706-3d9983ab-7cb1-4b27-a016-30c039d84e34.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;图像的块引用部分</a><br>
<a href="https://youtu.be/Etskjw7a5zo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931461-0979b821-315a-41dd-86f1-31d169b7c127.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;链接到元素、垂直文本对齐、Markdown 样式</a><br>
<a href="https://youtu.be/2Y8OhkGiTHg" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/152585752-7eb0371f-0bab-40f6-a194-3b48e5811735.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Excalidraw 原生超链接使用指南</a><br>
</details>
<details><summary>强大工具</summary>
<a href="https://youtu.be/NOuddK6xrr8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;便签(自动换行)</a><br>
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;本地字体</a><br>
<a href="https://youtu.be/vlC1-iBvIfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/199207784-8bbe14e0-7d10-47d7-971d-20dce8dbd659.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;SVG 导入</a><br>
<a href="https://youtu.be/7gu4ETx7zro" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/202916770-28f2fa64-1ba2-4b40-a7fe-d721b42634f7.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;OCR</a><br>
<a href="https://youtu.be/U2LkBRBk4LY" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/159369910-6371f08d-b5fa-454d-9c6c-948f7e7a7d26.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;绑定/解绑文本与容器,前置标签自定义导出</a><br>
<a href="https://youtu.be/uZz5MgzWXiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/211054371-8872e01a-77d6-4afc-a0c2-86a55410a8d3.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;自定义笔支持</a><br>
</details>
<details><summary>生活质量改善</summary>
<a href="https://youtu.be/qbPIAZguJeo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/151705333-54e9ffd2-0bd7-4d02-b99e-0bd4e4708d4d.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;移动支持</a><br>
<a href="https://youtu.be/2v9TZmQNO8c" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/153676009-6f86b2d7-c248-49a2-b802-be21c6999e4f.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;托盘模式和可自定义调色板</a><br>
<a href="https://youtu.be/xHPGWR3m0c8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/154821232-a404b6cf-72fb-4ce4-9d53-619132dce491.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;压缩 JSON 和改进的保存/同步支持</a><br>
<a href="https://youtu.be/gMIKXyhS-dM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931428-b2269fd9-87bd-43ab-8558-5572f40dff93.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Obsidian 工具面板</a><br>
<a href="https://youtu.be/4N6efq1DtH0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/158008902-12c6a851-237e-4edd-a631-d48e81c904b2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;橡皮擦、左利手模式、改进的文件名配置</a><br>
</details>
### Beta 测试
该插件遵循每月发布的计划。如果您希望获得更频繁的更新包括新功能例如excalidraw.com 上的新内容,但尚未在 Obsidian 中提供)和小的 bug 修复,请加入 beta 社区。
[![缩略图 - 20240803 Excalidraw 发布方法(自定义)](https://github.com/user-attachments/assets/ab40648c-f73f-4bda-a416-52839f918f2a)](https://youtu.be/2poSS-Z91lY)
[![Excalidraw 插件发布策略(手机)](https://github.com/user-attachments/assets/87f1f379-782c-4c32-8b5b-d27fe2d3ac4b)](https://github.com/user-attachments/assets/120a0790-7239-48ae-bfbd-eb249f8b518d)
---
## 功能
- 该插件将 Excalidraw 无缝集成到 Obsidian 中,包括命令面板操作、文件资源管理器功能、选项菜单命令和工具栏按钮。
- 在工具栏按钮或文件资源管理器中 <kbd>CTRL/CMD+鼠标左键</kbd> 以在新面板中创建/打开绘图。
### 设置
设置将允许您根据需要自定义 Excalidraw。该插件提供了大量设置。我尝试为这些设置添加有意义的解释所以请耐心查找对于大多数请求已经存在相关设置。
插件设置分为以下几个部分:
- **基本设置**:例如使用的默认文件夹。
- **保存**:压缩和自动保存间隔。
- **文件名**:配置自动生成的 Excalidraw 文件名。
- **显示**:影响 Excalidraw 处理的设置(例如:左利手模式、主题设置、鼠标滚轮和捏合缩放设置、适应缩放设置)。
- **链接和嵌入**:影响链接和嵌入项在 Excalidraw 画布上行为的设置。
- **Markdown 嵌入设置**:这些设置控制从您的 Vault 嵌入到 Excalidraw 绘图中的 Markdown 文档的行为。
- **嵌入与导出**:控制 Excalidraw 图像在嵌入到 Markdown 文档时的显示方式的设置。
- **自动导出设置**:您可以配置 Excalidraw 在每次保存时创建绘图的 PNG 或 SVG 副本。
- **兼容性功能**:如果您在 Obsidian 之外编辑 Excalidraw 绘图(例如在 LogSeq、Visual Studio、网页等请检查这些设置。
- **实验性功能**:有一些高级功能作为“巧妙”的 hacks 实现,包括定义本地字体、添加自定义图标以区分 Obsidian 文件资源管理器中的 Excalidraw 文件、OCR 设置等。
- **已安装脚本的设置**:从脚本库安装的一些脚本附带设置。脚本设置在您第一次运行脚本时安装。因此,要访问脚本的设置,请安装脚本,首次运行后在插件设置中查找设置。
#### 模板
- 新绘图的模板。该模板将恢复笔画属性。这意味着您可以在模板中设置笔画颜色、笔画宽度、不透明度、字体系列、字体大小、填充样式、笔画样式等的默认值。这同样适用于 ExcalidrawAutomate。
- 通过模板,您可以自定义 Excalidraw 使用的调色板。
- 切换到 Markdown 视图。
- 滚动到文件底部,找到 `"AppState": {`
- 在 AppState 部分末尾找到 `"customColorPalette": {`
- 您可以通过添加以下三个变量中的任何一个或全部来指定 Excalidraw 使用的 3 个调色板:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`
- 在每个变量的数组中添加有效 HTML 颜色的逗号分隔列表(例如,`#FF0000` 表示红色)。
- 有关更多帮助,请查看我上面的录像。
#### 导出
- 如果便携性对您很重要:
- 自动导出 SVG 和/或 PNG 文件,包括同步保持功能,这样您可以将 SVG/PNG 嵌入到文档中,而不是嵌入 Excalidraw 文件。
- 您可以通过添加 `excalidraw-autoexport` 前置字段键来覆盖单个文件的导出设置。该键的有效值为 `none``both``png``svg`
- 指定嵌入绘图的默认宽度。
- 兼容性功能以自动导出和保持同步 Markdown Excalidraw 文件及旧版 `.excalidraw` 文件。
- 实验性功能可在文件资源管理器中添加自定义标签以标记绘图文件。
- 启用/禁用自动保存。
### 将您的绘图嵌入到 Markdown 文档中
- 您可以使用以下格式自定义嵌入图像的大小和位置:
- `![[image.excalidraw|100]]`
- `![[image.excalidraw|100x100]]`
- `![[image.excalidraw|100|left]]`
- `![[image.excalidraw|right-wrap]]`
- `![[<filename.excalidraw>|<width>x<height>|<alignment>]]`
- 您可以通过 CSS 添加自定义 [对齐方式](https://www.scaler.com/topics/align-image-in-html/)。
- 出现在 `<alignment>` 中的任何文本将被添加到渲染的 SVG 元素样式和包装 DIV 元素中。
- 有关更多信息,请参见 [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css)。
- Excalidraw 绘图在 Obsidian Publish 中不显示。如果您希望在发布的文档中使用 Excalidraw可以在插件设置中的 `Embed & Export` 下进行配置,以便在创建新文件时自动将绘图的 PNG 或 SVG 版本插入到文档中。请参见 `type of file to insert into document`
-`Export settings` 下,您还可以配置自动导出图像的深色和浅色版本,以便您的发布网站支持深色和浅色模式。
### 超链接和拖放支持
![](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/images/excalidraw-modifiers.png)
#### 超链接
- 支持超链接,例如:
- `https://zsolt.blog`
- `[Obsidian](https://obsidian.md)`,以及
- 内部链接,例如在绘图文本中使用 `[[My file in vault|Alias]]`
- 如果您启用了 Obsidian 设置中的“文件与链接/自动更新内部链接”,则文件移动或重命名时链接会自动更新。
- 绘图中的链接会出现在文档的反向链接中。
- 支持嵌入:
- `![[myfile#^blockref]]` 将绘图转换为该块的嵌入文本。
- `![[myfile#section]]` 也有效,这将嵌入该部分。
- 您还可以通过在嵌入后加上最大字符数的花括号来指定嵌入文本的换行,例如 `![[myfile#^blockref]]{40}` 将在 40 个字符处换行。
- 为了方便,您还可以使用命令面板将链接插入到绘图中。
- <kbd>CTRL/CMD + 鼠标悬停</kbd> 可以调出链接的 Obsidian 快速预览。(在 Mac 上为 <kbd>CTRL+CMD+鼠标悬停</kbd>)。
- 使用块引用,您还可以在其他文档中引用和嵌入绘图中出现的文本。
#### 拖放支持
- 您可以从 Obsidian 文件资源管理器中拖动文件,它们将成为 Excalidraw 中指向这些文件的链接。有关各种修饰键组合,请参见上面的表格。
- 注意:将图像锚定到其 100% 尺寸是一个非常小众的功能,具有非常特定的行为,我主要是为自己开发的。
- (甚至 Excalidraw Obsidian 中的其他功能更是如此 - 也是主要为自己开发的 😉)。
- 每次打开 Excalidraw 绘图时,这将重置您嵌入的图像为 100% 尺寸,或者如果您在画布上嵌入了使用此功能插入的 Excalidraw 绘图,每次更新嵌入的绘图时,它将缩放回 100% 尺寸。
- 这意味着即使您在绘图中调整了图像的大小,下次打开文件或修改原始嵌入对象时,它也会重置为 100%。此功能在将绘图分解为单独的 Excalidraw 文件时非常有用,但当它们组合到单个画布上时,您希望各个部分保持其实际大小。我使用此功能从原子绘图构建“一页书”摘要。
- 您可以将文本从 Markdown 视图拖放到 Excalidraw 中。
- 您可以从浏览器中拖放网页地址,它们将成为链接。
- 您可以拖放 YouTube 链接和缩略图,它们将在 Excalidraw 中成为带缩略图的 YouTube 链接。
### LaTeX
使用命令面板操作“插入 LaTeX 公式”插入 LaTeX 公式。您可以在 Markdown 视图中编辑公式,或者通过 <kbd>CTRL/CMD + 鼠标左键</kbd> 点击公式进行编辑。
### 图像支持
- 在 iOS 和 Android 上,您可以通过按下 Excalidraw 中的添加图像按钮从相机添加图像。
- 您可以将图像复制/粘贴到绘图中。图像将保存在您的 Vault 中。
- 您可以按照上面的说明拖放图像。
- URL 链接到网络上的图像:您可以从网页将图像拖放到 Excalidraw。如果在将图像拖放到 Excalidraw 时按住 CTRL 键,图像将不会保存到您的 Vault 中。Excalidraw 将从 URL 加载图像。请注意,如果您没有互联网连接,或者这些图像从互联网上被删除,它们也会从您的绘图中消失。
- 如果您将图像 URL 粘贴到 Excalidraw只需点击 URL 复制,然后在 Excalidraw 画布上点击粘贴),图像将以链接形式插入到网络图像上。同样,图像不会保存到您的 Vault 中,只有链接会被保存。
- 如果您拖放 YouTube 视频链接,它将转换为一个缩略图,并带有指向视频的元素链接。
### 引用图像部分的块
有关更多详细信息,请参见此 [视频](https://youtu.be/yZQoJg2RCKI)。
- 当通过链接引用 Excalidraw 文件中的画布元素时,可以使用:
- 元素 ID 或章节标题(即包含 `# <章节标题>` 的文本元素)
- 例如 `[[file#^elementID]]`
- 您可以添加 `group=` 前缀,
- 例如 `[[file#^group=elementID]]`,或
- `area=` 前缀,
- 例如 `[[file#area=Section heading]]`
- 如果找到 `group=` 前缀Excalidraw 将选择与通过元素 ID块引用或章节标题引用的元素在同一组中的元素。
- 如果找到 `area=` 前缀Excalidraw 将在引用的元素周围插入图像的剪切部分。
- 请注意,当将 Excalidraw 嵌入为 PNG 到您的 Markdown 文档时,不支持 `area=` 选择器。
- 引用文本元素的元素 ID 而不带 `group=``area=` 前缀将以普通文本嵌入该元素。引用非文本元素(例如矩形、椭圆等)而不带 `group=``area=` 前缀将导致 Obsidian 错误,因为这些元素 ID 在 Excalidraw Markdown 文件中不能够作为块引用。
### Markdown
- 从 1.2.0 版本开始,绘图文件存储在 Markdown 文件中。
- 您可以为绘图添加标签。
- 您可以在绘图的 YAML 前置字段中添加元数据。
- 您在前置字段和 `# Text Elements` 标题之间添加的任何内容将被 Excalidraw 忽略,即您可以在这里添加任何内容,它将作为文档的一部分被保留。
- Excalidraw 文档现在会在图形视图中显示。
- 以下前置字段键将自定义绘图的显示方式 - 覆盖一般设置:
- `excalidraw-link-prefix: "📍"` 内部链接的预览前缀
- `excalidraw-url-prefix: "🌐"` 外部链接的预览前缀
- `excalidraw-link-brackets: true|false` 是否在预览中显示链接周围的括号
- `excalidraw-default-mode: view|zen` 默认以查看模式或禅模式打开此文档。默认查看模式非常适合演示幻灯片。
- 前置字段标签用于在文件级别自定义图像导出 [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519)。如果这些键存在,它们将覆盖默认的 Excalidraw 嵌入和导出设置。
- `excalidraw-export-transparent: true` true == 透明 / false == 有背景。
- `excalidraw-export-dark` true == 深色模式 / false == 浅色模式。
- `excalidraw-export-padding`:指定图像的导出边距。
- `excalidraw-export-pngscale`:这仅影响导出为 PNG。指定图像的导出比例。典型范围在 0.5 到 5 之间,但您也可以尝试其他值。
### 将完整的 Markdown 文件嵌入到您的绘图中
从 Obsidian 文件资源管理器中拖动所需文件,同时按住 <kbd>SHIFT</kbd> 将文件放到画布上。
- 使用命令面板操作:`从 Vault 插入 Markdown 文件`
- 使用自定义的 woff、woff2 或 TTF 字体来显示文档,您可以在 Excalidraw 设置中设置默认字体。
- 您可以为渲染 Markdown 文档的快照图像设置自定义 CSS。仅支持操作系统标准字体作为字体系列[Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list)、[Mac & iOS](https://developer.apple.com/fonts/system-fonts/)),此外,您可以使用上述设置添加一个额外的自定义字体。
- (要查看演示,请观看此 [视频](https://youtu.be/K6qZkTz8GHs) 并查看此
- [示例 CSS](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281))。
- 为了帮助样式设置,您可以查看 Excalidraw 创建的 Markdown 文档的 SVG 快照。
- 打开 Obsidian 开发者控制台 (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>),并
- 执行以下命令:`ExcalidrawAutomate.mostRecentMarkdownSVG`
- 您可以通过将以下前置字段键添加到您的 Markdown 文档,按文件控制嵌入 Markdown 文件的外观:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- 您可以在 [这里](https://www.w3schools.com/colors/colors_names.asp) 找到 CSS 颜色名称。
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- 切换到 Markdown 视图或使用 <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> 点击图像以编辑嵌入的属性:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
### 自定义字体、自定义笔、OCR 支持、SVG 导入
- 在插件设置中,您可以添加自定义的本地字体。有关更多详细信息,请参见此 [视频](https://youtu.be/eKFmrSQhFA4)。
- 该插件包括使用 Taskbone OCR 的 OCR 支持。有关更多详细信息,请参见此 [视频](https://youtu.be/7gu4ETx7zro)。
- 您可以将 SVG 文件转换为 Excalidraw 绘图(有一些限制)。有关更多详细信息,请参见此 [视频](https://youtu.be/vlC1-iBvIfo)。
- 您可以定义自定义笔和荧光笔,并将其固定到侧边栏。有关更多详细信息,请参见此 [视频](https://youtu.be/OjNhjaH2KjI)。使用 ExcalidrawAutomate您可以添加对 [自动切换](<ea-scripts/Auto Draw for Pen.md>) 笔的支持,以及对 [硬件橡皮擦按钮](<ea-scripts/Hardware Eraser Support.md>) 的支持。
### 脚本引擎
- 从 1.5.0 版本开始,您可以轻松执行 ExcalidrawAutomate 宏,并为它们分配命令面板快捷键,使用脚本引擎。您可以在 [这里](ea-scripts/README.md) 找到介绍视频和不断增加的可安装脚本库。
- 您可以通过将脚本和随附的 SVG 图标文件移动到文件夹中,将脚本组织成组,放在 Excalidraw 的 Obsidian 工具面板中。请参见演示 [视频](https://youtu.be/wTtaXmRJ7wg?t=16)。
### 其他
- 左利手模式
- 包含完整的
- [QuickAdd](https://github.com/chhoumann/quickadd)
- [Templater](https://silentvoid13.github.io/Templater/) 和
- [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) 支持,通过 ExcalidrawAutomate 实现。
- 查看 [详细帮助 + 示例](https://zsviczian.github.io/obsidian-excalidraw-plugin/)。
- 我还有一个 [YouTube ExcalidrawAutomate 播放列表](https://www.youtube.com/playlist?list=PL6mqgtMZ4NP1IR4nXxSlMA4PA5E-qpyHZ),里面有很多示例。
- 需要 Obsidian Sync 订阅:完整的绘图文件历史记录和设备之间的同步。
- 多语言支持:如果您想通过翻译插件来帮助,请与我联系。
## 反馈、问题、想法、问题
请在 [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian) 上参与关于 Excalidraw 插件的讨论。
请前往 [GitHub](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) 报告错误或请求增强功能。
---
## 感谢支持
如果您喜欢 Excalidraw请通过在 [https://ko-fi/zsolt](https://ko-fi.com/zsolt) 上请我喝杯咖啡来支持我的工作和热情。
请通过在 Twitter、Reddit 或其他您常用的社交媒体平台上分享 Obsidian Excalidraw 插件来帮助传播消息。
您可以在 Twitter 上找到我 [@zsviczian](https://twitter.com/zsviczian),以及我的博客 [zsolt.blog](https://zsolt.blog)。
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="200">](https://ko-fi.com/zsolt)
---
## Excalidraw 的朋友们
如果您喜欢 Excalidraw可以考虑尝试 [ExcaliBrain](https://github.com/zsviczian/excalibrain)(也可通过 Obsidian 社区插件获得)。
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>

773
docs/API/ExcalidrawAutomate.d.ts vendored Normal file
View File

@@ -0,0 +1,773 @@
/// <reference types="react" />
import ExcalidrawPlugin from "src/main";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
export declare class ExcalidrawAutomate {
/**
* Utility function that returns the Obsidian Module object.
*/
get obsidian(): typeof obsidian_module;
get DEVICE(): DeviceType;
getAttachmentFilepath(filename: string): Promise<string>;
/**
* Prompts the user with a dialog to select new file action.
* - create markdown file
* - create excalidraw file
* - cancel action
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
* targetPane control which leaf will be used for the new file.
* Returns the TFile for the new file or null if the user cancelled the action.
* @param newFileNameOrPath
* @param shouldOpenNewFile
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @param parentFile
* @returns
*/
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
/**
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
* @param origo // the currently active leaf, the origin of the new leaf
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @returns
*/
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
/**
* Returns the editor or leaf.view of the currently active embedded obsidian file.
* If view is not provided, ea.targetView is used.
* If the embedded file is a markdown document the function will return
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
* @param view
* @returns
*/
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
view: any;
} | {
file: TFile;
editor: Editor;
} | null;
plugin: ExcalidrawPlugin;
elementsDict: {
[key: string]: any;
};
imagesDict: {
[key: FileId]: any;
};
mostRecentMarkdownSVG: SVGSVGElement;
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
strokeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness?: StrokeRoundness;
roundness: null | {
type: RoundnessType;
value?: number;
};
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
};
canvas: {
theme: string;
viewBackgroundColor: string;
gridSize: number;
};
colorPalette: {};
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
/**
*
* @returns the last recorded pointer position on the Excalidraw canvas
*/
getViewLastPointerPosition(): {
x: number;
y: number;
};
/**
*
* @returns
*/
getAPI(view?: ExcalidrawView): ExcalidrawAutomate;
/**
* @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
* @returns
*/
setFillStyle(val: number): "hachure" | "cross-hatch" | "solid";
/**
* @param val //0:"solid", 1:"dashed", 2:"dotted"
* @returns
*/
setStrokeStyle(val: number): "solid" | "dashed" | "dotted";
/**
* @param val //0:"round", 1:"sharp"
* @returns
*/
setStrokeSharpness(val: number): "round" | "sharp";
/**
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
* @returns
*/
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "Local Font";
/**
* @param val //0:"light", 1:"dark"
* @returns
*/
setTheme(val: number): "light" | "dark";
/**
* @param objectIds
* @returns
*/
addToGroup(objectIds: string[]): string;
/**
* @param templatePath
*/
toClipboard(templatePath?: string): Promise<void>;
/**
* @param file: TFile
* @returns ExcalidrawScene
*/
getSceneFromFile(file: TFile): Promise<{
elements: ExcalidrawElement[];
appState: AppState;
}>;
/**
* get all elements from ExcalidrawAutomate elementsDict
* @returns elements from elementsDict
*/
getElements(): ExcalidrawElement[];
/**
* get single element from ExcalidrawAutomate elementsDict
* @param id
* @returns
*/
getElement(id: string): ExcalidrawElement;
/**
* create a drawing and save it to filename
* @param params
* filename: if null, default filename as defined in Excalidraw settings
* foldername: if null, default folder as defined in Excalidraw settings
* @returns
*/
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;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string;
}): Promise<string>;
/**
*
* @param templatePath
* @param embedFont
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
* @param theme
* @returns
*/
createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<SVGSVGElement>;
/**
*
* @param templatePath
* @param scale
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
* @param theme
* @returns
*/
createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<any>;
/**
*
* @param text
* @param lineLen
* @returns
*/
wrapText(text: string, lineLen: number): string;
private boxedElement;
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addRect(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addDiamond(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEllipse(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addBlob(topX: number, topY: number, width: number, height: number): string;
/**
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string): void;
/**
*
* @param topX
* @param topY
* @param text
* @param formatting
* box: if !null, text will be boxed
* @param id
* @returns
*/
addText(topX: number, topY: number, text: string, formatting?: {
wrapAt?: number;
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
}, id?: string): string;
/**
*
* @param points
* @returns
*/
addLine(points: [[x: number, y: number]]): string;
/**
*
* @param points
* @param formatting
* @returns
*/
addArrow(points: [x: number, y: number][], formatting?: {
startArrowHead?: string;
endArrowHead?: string;
startObjectId?: string;
endObjectId?: string;
}): string;
/**
*
* @param topX
* @param topY
* @param imageFile
* @returns
*/
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor?: boolean): Promise<string>;
/**
*
* @param topX
* @param topY
* @param tex
* @returns
*/
addLaTex(topX: number, topY: number, tex: string): Promise<string>;
/**
*
* @param objectA
* @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
* @param objectB
* @param connectionB when passed null, Excalidraw will automatically decide
* @param formatting
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* padding:
* @returns
*/
connectObjects(objectA: string, connectionA: ConnectionPoint | null, objectB: string, connectionB: ConnectionPoint | null, formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): string;
/**
* Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
* @param lineId id of the line or arrow object in elementsDict
* @param label the label text
* @returns undefined (if unsuccessful) or the id of the new text element
*/
addLabelToLine(lineId: string, label: string): string;
/**
* clear elementsDict and imagesDict only
*/
clear(): void;
/**
* clear() + reset all style values to default
*/
reset(): void;
/**
* returns true if MD file is an Excalidraw file
* @param f
* @returns
*/
isExcalidrawFile(f: TFile): boolean;
targetView: ExcalidrawView;
/**
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
* if view is null or undefined, the function will first try setView("active"), then setView("first").
* @param view
* @returns targetView
*/
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
/**
*
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
*/
getExcalidrawAPI(): any;
/**
* get elements in View
* @returns
*/
getViewElements(): ExcalidrawElement[];
/**
*
* @param elToDelete
* @returns
*/
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean;
/**
* get the selected element in the view, if more are selected, get the first
* @returns
*/
getViewSelectedElement(): any;
/**
*
* @returns
*/
getViewSelectedElements(): any[];
/**
*
* @param el
* @returns TFile file handle for the image element
*/
getViewFileForImageElement(el: ExcalidrawElement): TFile | null;
/**
* copies elements from view to elementsDict for editing
* @param elements
*/
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;
/**
*
* @param forceViewMode
* @returns
*/
viewToggleFullScreen(forceViewMode?: boolean): void;
setViewModeEnabled(enabled: boolean): void;
/**
* This function gives you a more hands on access to Excalidraw.
* @param scene - The scene you want to load to Excalidraw
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
* @returns
*/
viewUpdateScene(scene: {
elements?: ExcalidrawElement[];
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
storeAction?: "capture" | "none" | "update";
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view
* @param objectA ID of the element
* @param connectionA
* @param connectionB
* @param formatting
* @returns
*/
connectObjectWithViewSelectedElement(objectA: string, connectionA: ConnectionPoint | null, connectionB: ConnectionPoint | null, formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): boolean;
/**
* zoom tarteView to fit elements provided as input
* elements === [] will zoom to fit the entire scene
* selectElements toggles whether the elements should be in a selected state at the end of the operation
* @param selectElements
* @param elements
*/
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
/**
* Adds elements from elementsDict to the current view
* @param repositionToCursor default is false
* @param save default is true
* @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
* are added at the bottom of the stack or the top of the stack of elements already in the view
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
* position in the stack of elements in the view even if modified using EA
* default is false, i.e. the new elements get to the bottom of the stack
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
* @returns
*/
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
/**
* Register instance of EA to use for hooks with TargetView
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
* Using this event you can set a different instance of Excalidraw Automate for hooks
* @returns true if successful
*/
registerThisAsViewEA(): boolean;
/**
* Sets the targetView EA to window.ExcalidrawAutomate
* @returns true if successful
*/
deregisterThisAsViewEA(): boolean;
/**
* If set, this callback is triggered when the user closes an Excalidraw view.
*/
onViewUnloadHook: (view: ExcalidrawView) => void;
/**
* If set, this callback is triggered, when the user changes the view mode.
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
*/
onViewModeChangeHook: (isViewModeEnabled: boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void;
/**
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
*/
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
* If set, this callback is triggered, when the user clicks a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
*/
onLinkClickHook: (element: ExcalidrawElement, linkText: string, event: MouseEvent, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
* You can use this callback in case you want to do something additional when the onDrop event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
*/
onDropHook: (data: {
ea: ExcalidrawAutomate;
event: React.DragEvent<HTMLDivElement>;
draggable: any;
type: "file" | "text" | "unknown";
payload: {
files: TFile[];
text: string;
};
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
*/
onPasteHook: (data: {
ea: ExcalidrawAutomate;
payload: ClipboardData;
event: ClipboardEvent;
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* if set, this callback is triggered, when an Excalidraw file is opened
* You can use this callback in case you want to do something additional when the file is opened.
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
*/
onFileOpenHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile;
view: ExcalidrawView;
}) => Promise<void>;
/**
* if set, this callback is triggered, when an Excalidraw file is created
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
*/
onFileCreateHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile;
view: ExcalidrawView;
}) => Promise<void>;
/**
* If set, this callback is triggered whenever the active canvas color changes
*/
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
color: string) => void;
/**
* utility function to generate EmbeddedFilesLoader object
* @param isDark
* @returns
*/
getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader;
/**
* utility function to generate ExportSettings object
* @param withBackground
* @param withTheme
* @returns
*/
getExportSettings(withBackground: boolean, withTheme: boolean): ExportSettings;
/**
* get bounding box of elements
* bounding box is the box encapsulating all of the elements completely
* @param elements
* @returns
*/
getBoundingBox(elements: ExcalidrawElement[]): {
topX: number;
topY: number;
width: number;
height: number;
};
/**
* elements grouped by the highest level groups
* @param elements
* @returns
*/
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
* @param elements
* @returns
*/
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
/**
* @param element
* @param a
* @param b
* @param gap
* @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): Point[];
/**
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
* @param elements
* @returns null or the groupId
*/
getCommonGroupForElements(elements: ExcalidrawElement[]): string;
/**
* Gets all the elements from elements[] that share one or more groupIds with element.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* See OCR plugin for example on how to use scriptSettings
* Set by the ScriptEngine
*/
activeScript: string;
/**
*
* @returns script settings. Saves settings in plugin settings, under the activeScript key
*/
getScriptSettings(): {};
/**
* sets script settings.
* @param settings
* @returns
*/
setScriptSettings(settings: any): Promise<void>;
/**
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
* @param file
* @param openState - if not provided {active: true} will be used
* @returns
*/
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
/**
* measure text size based on current style settings
* @param text
* @returns
*/
measureText(text: string): {
width: number;
height: number;
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* @param imageElement an image element from the active scene on targetView
*/
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
width: number;
height: number;
}>;
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
* if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
* @param requiredVersion
* @returns
*/
verifyMinimumPluginVersion(requiredVersion: string): boolean;
/**
* Check if view is instance of ExcalidrawView
* @param view
* @returns
*/
isExcalidrawView(view: any): boolean;
/**
* sets selection in view
* @param elements
* @returns
*/
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
/**
* @returns an 8 character long random id
*/
generateElementId(): string;
/**
* @param element
* @returns a clone of the element with a new id
*/
cloneElement(element: ExcalidrawElement): ExcalidrawElement;
/**
* Moves the element to a specific position in the z-index
*/
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
/**
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hexStringToRgb(color: string): number[];
/**
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHexString(color: number[]): string;
/**
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hslToRgb(color: number[]): number[];
/**
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHsl(color: number[]): number[];
/**
*
* @param color
* @returns
*/
colorNameToHex(color: string): string;
/**
* https://github.com/lbragile/ColorMaster
* @param color
* @returns
*/
getCM(color: TInput): ColorMaster;
importSVG(svgString: string): boolean;
}
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
export declare function destroyExcalidrawAutomate(): void;
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
w: number;
h: number;
baseline: number;
};
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
x: number;
y: number;
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
export declare const search: (view: ExcalidrawView) => Promise<void>;
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
export declare const cloneElement: (el: ExcalidrawElement) => any;
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;

View File

@@ -2,36 +2,110 @@
## Attributes and functions overview
Here's the interface implemented by ExcalidrawAutomate:
```typescript
export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
plugin: ExcalidrawPlugin;
targetView: ExcalidrawView = null; //the view currently edited
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
style: {
strokeColor: string; //https://www.w3schools.com/colors/default.asp
backgroundColor: string;
angle: number; //radian
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
strokeWidth: number;
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp"
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
fontSize: number;
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; //"dark"|"light"
viewBackgroundColor: string;
gridSize: number;
};
You can find the source file here: [ExcalidrawAutomate.d.ts](ExcalidrawAutomate.d.ts).
```javascript
/// <reference types="react" />
import ExcalidrawPlugin from "src/main";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
export declare class ExcalidrawAutomate {
/**
* Utility function that returns the Obsidian Module object.
*/
get obsidian(): typeof obsidian_module;
get DEVICE(): DeviceType;
getAttachmentFilepath(filename: string): Promise<string>;
/**
* Prompts the user with a dialog to select new file action.
* - create markdown file
* - create excalidraw file
* - cancel action
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
* targetPane control which leaf will be used for the new file.
* Returns the TFile for the new file or null if the user cancelled the action.
* @param newFileNameOrPath
* @param shouldOpenNewFile
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @param parentFile
* @returns
*/
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
/**
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
* @param origo // the currently active leaf, the origin of the new leaf
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @returns
*/
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
/**
* Returns the editor or leaf.view of the currently active embedded obsidian file.
* If view is not provided, ea.targetView is used.
* If the embedded file is a markdown document the function will return
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
* @param view
* @returns
*/
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
view: any;
} | {
file: TFile;
editor: Editor;
} | null;
plugin: ExcalidrawPlugin;
elementsDict: {
[key: string]: any;
};
imagesDict: {
[key: FileId]: any;
};
mostRecentMarkdownSVG: SVGSVGElement;
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
strokeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness?: StrokeRoundness;
roundness: null | {
type: RoundnessType;
value?: number;
};
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
};
canvas: {
theme: string;
viewBackgroundColor: string;
gridSize: number;
};
colorPalette: {};
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
/**
*
* @returns the last recorded pointer position on the Excalidraw canvas
*/
getViewLastPointerPosition(): {
x: number;
y: number;
};
/**
*
* @returns
@@ -71,9 +145,17 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param templatePath
*/
toClipboard(templatePath?: string): Promise<void>;
/**
* @param file: TFile
* @returns ExcalidrawScene
*/
getSceneFromFile(file: TFile): Promise<{
elements: ExcalidrawElement[];
appState: AppState;
}>;
/**
* get all elements from ExcalidrawAutomate elementsDict
* @returns elements from elemenetsDict
* @returns elements from elementsDict
*/
getElements(): ExcalidrawElement[];
/**
@@ -101,10 +183,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-svgpadding"?: number;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string;
}): Promise<string>;
/**
*
@@ -134,6 +220,16 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
wrapText(text: string, lineLen: number): string;
private boxedElement;
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
@@ -170,6 +266,11 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
addBlob(topX: number, topY: number, width: number, height: number): string;
/**
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string): void;
/**
*
* @param topX
@@ -184,9 +285,11 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
wrapAt?: number;
width?: number;
height?: number;
textAlign?: string;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
}, id?: string): string;
/**
*
@@ -213,7 +316,8 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param imageFile
* @returns
*/
addImage(topX: number, topY: number, imageFile: TFile): Promise<string>;
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor?: boolean): Promise<string>;
/**
*
* @param topX
@@ -262,12 +366,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
isExcalidrawFile(f: TFile): boolean;
targetView: ExcalidrawView;
/**
*
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
* if view is null or undefined, the function will first try setView("active"), then setView("first").
* @param view
* @returns
* @returns targetView
*/
setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView;
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
/**
*
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
@@ -311,6 +417,19 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
viewToggleFullScreen(forceViewMode?: boolean): void;
setViewModeEnabled(enabled: boolean): void;
/**
* This function gives you a more hands on access to Excalidraw.
* @param scene - The scene you want to load to Excalidraw
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
* @returns
*/
viewUpdateScene(scene: {
elements?: ExcalidrawElement[];
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view
* @param objectA ID of the element
@@ -325,6 +444,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): boolean;
/**
* zoom tarteView to fit elements provided as input
* elements === [] will zoom to fit the entire scene
* selectElements toggles whether the elements should be in a selected state at the end of the operation
* @param selectElements
* @param elements
*/
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
/**
* Adds elements from elementsDict to the current view
* @param repositionToCursor default is false
@@ -334,9 +461,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
* position in the stack of elements in the view even if modified using EA
* default is false, i.e. the new elements get to the bottom of the stack
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
* @returns
*/
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean): Promise<boolean>;
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
/**
* Register instance of EA to use for hooks with TargetView
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
@@ -362,7 +490,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return true, it will stop the native excalidraw onLinkHover management flow.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
*/
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
@@ -394,6 +522,49 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
y: number;
};
}) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
*/
onPasteHook: (data: {
ea: ExcalidrawAutomate;
payload: ClipboardData;
event: ClipboardEvent;
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* if set, this callback is triggered, when an Excalidraw file is opened
* You can use this callback in case you want to do something additional when the file is opened.
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
*/
onFileOpenHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile;
view: ExcalidrawView;
}) => Promise<void>;
/**
* if set, this callback is triggered, when an Excalidraw file is created
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
*/
onFileCreateHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile;
view: ExcalidrawView;
}) => Promise<void>;
/**
* If set, this callback is triggered whenever the active canvas color changes
*/
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
color: string) => void;
/**
* utility function to generate EmbeddedFilesLoader object
* @param isDark
@@ -431,6 +602,15 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
/**
* @param element
* @param a
* @param b
* @param gap
* @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): Point[];
/**
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
* @param elements
@@ -445,14 +625,12 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param a
* @param b
* @param gap
* @returns 2 or 0 intersection points between line going through `a` and `b`
* and the `element`, in ascending order of distance from `a`.
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* See OCR plugin for example on how to use scriptSettings
* Set by the ScriptEngine
@@ -472,9 +650,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
/**
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
* @param file
* @param openState - if not provided {active: true} will be used
* @returns
*/
openFileInNewOrAdjacentLeaf(file: TFile): WorkspaceLeaf;
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
/**
* measure text size based on current style settings
* @param text
@@ -484,6 +663,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
width: number;
height: number;
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* @param imageElement an image element from the active scene on targetView
*/
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
width: number;
height: number;
}>;
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
@@ -503,7 +690,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param elements
* @returns
*/
selectElementsInView(elements: ExcalidrawElement[]): void;
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
/**
* @returns an 8 character long random id
*/
@@ -518,25 +705,25 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
/**
*
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hexStringToRgb(color: string): number[];
/**
*
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHexString(color: number[]): string;
/**
*
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hslToRgb(color: number[]): number[];
/**
*
* Deprecated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
@@ -547,5 +734,47 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
colorNameToHex(color: string): string;
}```
/**
* https://github.com/lbragile/ColorMaster
* @param color
* @returns
*/
getCM(color: TInput): ColorMaster;
importSVG(svgString: string): boolean;
}
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
export declare function destroyExcalidrawAutomate(): void;
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
w: number;
h: number;
baseline: number;
};
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
x: number;
y: number;
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
export declare const search: (view: ExcalidrawView) => Promise<void>;
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
export declare const cloneElement: (el: ExcalidrawElement) => any;
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;
```

View File

@@ -88,4 +88,4 @@ 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.
This is relevant when using the `addArrow()` and the `connectObjects()` functions.

View File

@@ -21,7 +21,7 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
You can change the 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 that. There would be no point in setting all these parameters each time you add an element.
### Before we dive deeper, here are three a simple example [Templater](https://github.com/SilentVoid13/Templater) scripts
### Before we dive deeper, here are three simple example [Templater](https://github.com/SilentVoid13/Templater) 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.

View File

@@ -22,7 +22,7 @@ Places the generated drawing to the clipboard. Useful when you don't want to cre
```typescript
getElements():ExcalidrawElement[];
```
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is useful when working with ExcalidrawRef.
### getElement()
```typescript
@@ -156,7 +156,7 @@ You first need to set the view calling `setView()`.
Gets the array of selected elements in the scene. Returns [] if no elements are selected.
Note: you can call `getExcalidrawAPI().getSceneElements()` to retreive all the elements in the scene.
Note: you can call `getExcalidrawAPI().getSceneElements()` to retrieve all the elements in the scene.
#### viewToggleFullScreen()
```typescript
@@ -178,7 +178,7 @@ Same as `connectObjects()`, but ObjectB is the currently selected element in the
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
```
Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
`repositionToCursor` dafault is false
`repositionToCursor` default is false
- true: the elements will be moved such that the center point of the elements will be aligned with the current position of the pointer on ExcalidrawView. You can point and place elements to a desired location in your drawing using this switch.
- false: elements will be positioned as defined by the x&y coordinates of each element.
@@ -204,7 +204,7 @@ onDropHook (data: {
```
Callback function triggered when an draggable item is dropped on Excalidraw.
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
The function should return a boolean value. True if the drop was handled by the hook and further native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
type of drop can be one of:
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string

View File

@@ -1,6 +1,6 @@
# [◀ 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.
This is a slightly more elaborate example. This will generate a mindmap from a tabulated outline.
### Output
![Drawing 2021-05-05 20 52 34](https://user-images.githubusercontent.com/14358394/117194124-00a69d00-ade4-11eb-8b75-5e18a9cbc3cd.png)

View File

@@ -21,7 +21,7 @@ This will allow you to assign hotkeys to your favorite scripts just like to any
## Script development
An Excalidraw script will automatically receive two objects:
- `ea`: The Script Enginge will initialize the `ea` object including setting the active view to the View from which the script was called.
- `ea`: The Script Engine will initialize the `ea` object including setting the active view to the View from which the script was called.
- `utils`: I have borrowed functions exposed on utils from [QuickAdd](https://github.com/chhoumann/quickadd/blob/master/docs/QuickAddAPI.md), though currently not all QuickAdd utility functions are implemented in Excalidraw. As of now, these are the available functions. See the example below for details.
- `inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}])`
- Opens a prompt that asks for an input. Returns a string with the input.

View File

@@ -8,7 +8,7 @@ With a little work, using ExcalidrawAutomate you can generate simple mindmaps, b
## API documentation
- **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)
- [Element Style](API/element_style.md)
- [Canvas Style](API/canvas_style.md)
- [Adding Objects](API/objects.md)
- [Utility Functions](API/utility.md)

View File

@@ -4,7 +4,7 @@
THIS SCRIPT REQUIRES EXCALIDRAW 1.5.15
The script will
1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and
1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and
2) will add the text to your drawing as a text element
I recommend also installing the [Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md) script as well.

View File

@@ -15,7 +15,7 @@ In the `Command Palette` installed scripts are prefixed with `Downloaded/`, thus
## Attention developers and hobby hackers
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/hobby-programmer.svg' align='left' style='background-color:whitesmoke; width:80px; margin-right:15px; margin-bottom:10px;'/>
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is availble (hope to build this feature in the future).
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is available (hope to build this feature in the future).
I would love to include your contribution in the script library. If you have a script of your own that you would like to share with the community, please open a [PR](https://github.com/zsviczian/obsidian-excalidraw-plugin/pulls) on GitHub. Be sure to include the following in your pull request
- The [script file](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) with a self explanetory name. The name of the file will be the name of the script in the Command Palette.
@@ -243,7 +243,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/OCR%20-%20Optical%20Character%20Recognition.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/OCR%20-%20Optical%20Character%20Recognition.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">REQUIRES EXCALIDRAW 1.5.15<br>The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.<br><mark>⚠ Note that you will need to manually paste your token into the script after the first run! ⚠</mark><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg'><br><iframe width="560" height="315" src="https://www.youtube.com/embed/W2NMzR8s4eE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/OCR%20-%20Optical%20Character%20Recognition.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">REQUIRES EXCALIDRAW 1.5.15<br>The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and 2) will add the text to your drawing as a text element.<br><mark>⚠ Note that you will need to manually paste your token into the script after the first run! ⚠</mark><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg'><br><iframe width="560" height="315" src="https://www.youtube.com/embed/W2NMzR8s4eE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Organic Line
```excalidraw-script-install

View File

@@ -0,0 +1,369 @@
/*
With This Script it is possible to make boolean Operations on Shapes.
The style of the resulting shape will be the style of the highest ranking Element that was used.
The ranking of the elements is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.
The ranking is also important for the difference operation, so a transparent object for example will cut a hole into a solid object.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png)
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.20")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const ShadowGroupMarker = "ShadowCloneOf-";
const elements = ea.getViewSelectedElements().filter(
el=>["ellipse", "rectangle", "diamond"].includes(el.type) ||
el.groupIds.some(id => id.startsWith(ShadowGroupMarker)) ||
(["line", "arrow"].includes(el.type) && el.roundness === null)
);
if(elements.length === 0) {
new Notice ("Select ellipses, rectangles or diamonds");
return;
}
const PolyBool = ea.getPolyBool();
const polyboolAction = await utils.suggester(["union (a + b)", "intersect (a && b)", "difference (a - b)", "reversed difference (b - a)", "xor"], [
PolyBool.union, PolyBool.intersect, PolyBool.difference, PolyBool.differenceRev, PolyBool.xor
], "What would you like todo with the object");
const shadowClones = elements.filter(element => element.groupIds.some(id => id.startsWith(ShadowGroupMarker)));
shadowClones.forEach(shadowClone => {
let parentId = shadowClone.groupIds
.filter(id => id.startsWith(ShadowGroupMarker))[0]
.slice(ShadowGroupMarker.length);
const shadowCloneIndex = elements.findIndex(element => element.id == parentId);
if (shadowCloneIndex == -1) return;
elements[shadowCloneIndex].backgroundColor = shadowClone.backgroundColor;
elements[shadowCloneIndex].fillStyle = shadowClone.fillStyle;
})
const borderElements = elements.filter(element => !element.groupIds.some(id => id.startsWith(ShadowGroupMarker)));
groups = ea.getMaximumGroups(borderElements);
groups = groups.map((group) => group.sort((a, b) => RankElement(b) - RankElement(a)));
groups.sort((a, b) => RankElement(b[0]) - RankElement(a[0]));
ea.style.strokeColor = groups[0][0].strokeColor;
ea.style.backgroundColor = groups[0][0].backgroundColor;
ea.style.fillStyle = groups[0][0].fillStyle;
ea.style.strokeWidth = groups[0][0].strokeWidth;
ea.style.strokeStyle = groups[0][0].strokeStyle;
ea.style.roughness = groups[0][0].roughness;
ea.style.opacity = groups[0][0].opacity;
const basePolygons = groups.shift().map(element => traceElement(element));
const toolPolygons = groups.flatMap(group => group.map(element => traceElement(element)));
const result = polyboolAction({
regions: basePolygons,
inverted: false
}, {
regions: toolPolygons,
inverted: false
});
const polygonHierachy = subordinateInnerPolygons(result.regions);
drawPolygonHierachy(polygonHierachy);
ea.deleteViewElements(elements);
ea.addElementsToView(false,false,true);
return;
function traceElement(element) {
const diamondPath = (diamond) => [
SxVEC(1/2, [0, diamond.height]),
SxVEC(1/2, [diamond.width, 0]),
addVec([SxVEC(1/2, [0, diamond.height]), ([diamond.width, 0])]),
addVec([SxVEC(1/2, [diamond.width, 0]), ([0, diamond.height])]),
SxVEC(1/2, [0, diamond.height])
];
const rectanglePath = (rectangle) => [
[0,0],
[0, rectangle.height],
[rectangle.width, rectangle.height],
[rectangle.width, 0],
[0, 0]
]
const ellipsePath = (ellipse) => {
const angle = ellipse.angle;
const width = ellipse.width;
const height = ellipse.height;
const ellipseAtPoint = (t) => {
const spanningVector = [width/2*Math.cos(t), height/2*Math.sin(t)];
const baseVector = [width/2, height/2];
return addVec([spanningVector, baseVector]);
}
let points = [];
step = (2*Math.PI)/64
for (let t = 0; t < 2*Math.PI; t = t + step) {
points.push(ellipseAtPoint(t));
}
return points;
}
let polygon;
let correctForPolygon = [0, 0];
switch (element.type) {
case "diamond":
polygon = diamondPath(element);
break;
case "rectangle":
polygon = rectanglePath(element);
break;
case "ellipse":
polygon = ellipsePath(element);
break;
case "line":
case "arrow":
if (element.angle != 0) {
let smallestX = 0;
let smallestY = 0;
element.points.forEach(point => {
if (point[0] < smallestX) smallestX = point[0];
if (point[1] < smallestY) smallestY = point[1];
});
polygon = element.points.map(point => {
return [
point[0] -= smallestX,
point[1] -= smallestY
];
});
correctForPolygon = [smallestX, smallestY];
break;
}
if (element.roundness) {
new Notice("This script does not work with curved lines or arrows yet!");
return [];
}
polygon = element.points;
default:
break;
}
if (element.angle == 0) return polygon.map(v => addVec([v, [element.x, element.y]]));
polygon = polygon.map(v => addVec([v, SxVEC(-1/2, [element.width, element.height])]));
polygon = rotateVectorsByAngle(polygon, element.angle);
return polygon.map(v => addVec([v, [element.x, element.y], SxVEC(1/2, [element.width, element.height]), correctForPolygon]));
}
function RankElement(element) {
let score = 0;
const backgroundRank = [
"dashed",
"none",
"hachure",
"zigzag",
"zigzag-line",
"cross-hatch",
"solid"
]
score += (backgroundRank.findIndex((fillStyle) => fillStyle == element.fillStyle) + 1) * 10;
if (element.backgroundColor == "transparent") score -= 100;
if (element.points && getVectorLength(element.points[element.points.length - 1]) > 8) score -= 100;
if (score < 0) score = 0;
score += element.opacity / 100;
return score;
}
function drawPolygonHierachy(polygonHierachy) {
const backgroundColor = ea.style.backgroundColor;
const strokeColor = ea.style.strokeColor;
const setInnerStyle = () => {
ea.style.backgroundColor = backgroundColor;
ea.style.strokeColor = "transparent";
}
const setBorderStyle = () => {
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = strokeColor;
}
const setFilledStyle = () => {
ea.style.backgroundColor = backgroundColor;
ea.style.strokeColor = strokeColor;
}
polygonHierachy.forEach(polygon => {
setFilledStyle();
let path = polygon.path;
path.push(polygon.path[0]);
if (polygon.innerPolygons.length === 0) {
ea.addLine(path);
return;
}
const outerBorder = path;
const innerPolygons = addInnerPolygons(polygon.innerPolygons);
path = path.concat(innerPolygons.backgroundPath);
path.push(polygon.path[0]);
setInnerStyle();
const backgroundId = ea.addLine(path);
setBorderStyle();
const outerBorderId = ea.addLine(outerBorder)
const innerBorderIds = innerPolygons.borderPaths.map(path => ea.addLine(path));
const allIds = [innerBorderIds, outerBorderId, backgroundId].flat();
ea.addToGroup(allIds);
const background = ea.getElement(backgroundId);
background.groupIds.push(ShadowGroupMarker + outerBorderId);
});
}
function addInnerPolygons(polygonHierachy) {
let firstPath = [];
let secondPath = [];
let borderPaths = [];
polygonHierachy.forEach(polygon => {
let path = polygon.path;
path.push(polygon.path[0]);
borderPaths.push(path);
firstPath = firstPath.concat(path);
secondPath.push(polygon.path[0]);
drawPolygonHierachy(polygon.innerPolygons);
});
return {
backgroundPath: firstPath.concat(secondPath.reverse()),
borderPaths: borderPaths
};
}
function subordinateInnerPolygons(polygons) {
const polygonObjectPrototype = (polygon) => {
return {
path: polygon,
innerPolygons: []
};
}
const insertPolygonIntoHierachy = (polygon, hierarchy) => {
for (let i = 0; i < hierarchy.length; i++) {
const polygonObject = hierarchy[i];
let inside = null;
let pointIndex = 0;
do {
inside = pointInPolygon(polygon[pointIndex], polygonObject.path);
pointIndex++
} while (inside === null);
if (inside) {
hierarchy[i].innerPolygons = insertPolygonIntoHierachy(polygon, hierarchy[i].innerPolygons);
return hierarchy;
}
}
polygon = polygonObjectPrototype(polygon);
for (let i = 0; i < hierarchy.length; i++) {
const polygonObject = hierarchy[i];
let inside = null;
let pointIndex = 0;
do {
inside = pointInPolygon(polygonObject.path[pointIndex], polygon.path);
pointIndex++
} while (inside === null);
if (inside) {
polygon.innerPolygons.push(hierarchy.splice(i, 1)[0]);
i--;
}
}
hierarchy.push(polygon);
return hierarchy;
}
let polygonHierachy = [];
polygons.forEach(polygon => {
polygonHierachy = insertPolygonIntoHierachy(polygon, polygonHierachy);
})
return polygonHierachy;
}
/**
* Checks if the given point lays in the polygon
* @param point array [x, y]
* @param polygon array [[x, y], ...]
* @returns true if inside, false if not, null if the point is on one of the polygons vertecies
*/
function pointInPolygon(point, polygon) {
const x = point[0];
const y = point[1];
let inside = false;
// odd even test if point is in polygon
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect =
yi > y !== yj > y &&
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) {
inside = !inside;
}
if ((x === xi && y === yi) || (x === xj && y === yj)) {
return null;
}
}
return inside;
}
function getVectorLength(vector) {
return Math.sqrt(vector[0]**2+vector[1]**2);
}
/**
* Adds two Vectors together
*/
function addVec(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
/**
* Returns the negative of the vector
*/
function negVec(vector) {
return [-vector[0], -vector[1]];
}
/**
* Multiplies Vector with a scalar
*/
function SxVEC(scalar, vector) {
return [vector[0] * scalar, vector[1] * scalar];
}
function rotateVector (vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function rotateVectorsByAngle(vectors, angle) {
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const rotationMatrix = [
[cosAngle, -sinAngle],
[sinAngle, cosAngle]
];
return applyTranformationMatrix(vectors, rotationMatrix);
}
function applyTranformationMatrix(vectors, transformationMatrix) {
const result = [];
for (const vector of vectors) {
const x = vector[0];
const y = vector[1];
const newX = transformationMatrix[0][0] * x + transformationMatrix[0][1] * y;
const newY = transformationMatrix[1][0] * x + transformationMatrix[1][1] * y;
result.push([newX, newY]);
}
return result;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,102 @@
/*
Connects two lines. Lines may be type of arrow or line. The resulting line will carry the style of the line higher in the drawing layers (bring to front the one you want to control the look and feel). Arrows are connected intelligently.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-concatenate-lines.png)
```js*/
const lines = ea.getViewSelectedElements().filter(el=>el.type==="line" || el.type==="arrow");
if(lines.length !== 2) {
new Notice ("Select two lines or arrows");
return;
}
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
const rotate = (point, element) => {
const [x1, y1] = point;
const x2 = element.x + element.width/2;
const y2 = element.y - element.height/2;
const angle = element.angle;
return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
];
}
const points = lines.map(
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
);
const last = (p) => p[p.length-1];
const first = (p) => p[0];
const distance = (p1,p2) => Math.sqrt((p1[0]-p2[0])**2+(p1[1]-p2[1])**2);
const distances = [
distance(first(points[0]),first(points[1])),
distance(first(points[0]),last (points[1])),
distance(last (points[0]),first(points[1])),
distance(last (points[0]),last (points[1]))
];
const connectDirection = distances.indexOf(Math.min(...distances));
let newPoints = [];
switch(connectDirection) {
case 0: //first-first
newPoints = [...points[0].reverse(),...points[1].slice(1)];
break;
case 1: //first-last
newPoints = [...points[0].reverse(),...points[1].reverse().slice(1)];
break;
case 2: //last-first
newPoints = [...points[0],...points[1].slice(1)];
break;
case 3: //last-last
newPoints = [...points[0],...points[1].reverse().slice(1)];
break;
}
["strokeColor", "backgrounColor", "fillStyle", "roundness", "roughness", "strokeWidth", "strokeStyle", "opacity"].forEach(prop=>{
ea.style[prop] = lines[1][prop];
})
ea.style.startArrowHead = null;
ea.style.endArrowHead = null;
ea.copyViewElementsToEAforEditing(lines);
ea.getElements().forEach(el=>{el.isDeleted = true});
const lineTypes = parseInt(lines.map(line => line.type === "line" ? '1' : '0').join(''),2);
switch (lineTypes) {
case 0: //arrow - arrow
ea.addArrow(
newPoints,
connectDirection === 0 //first-first
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].endArrowhead }
: connectDirection === 1 //first-last
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].startArrowhead }
: connectDirection === 2 //last-first
? { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].endArrowhead }
//3: last-last
: { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].startArrowhead }
);
break;
case 1: //arrow - line
reverse = connectDirection === 0 || connectDirection === 1;
ea.addArrow(newPoints,{
startArrowHead: reverse ? lines[0].endArrowhead : lines[0].startArrowhead,
endArrowHead: reverse ? lines[0].startArrowhead : lines[0].endArrowhead
});
break;
case 2: //line - arrow
reverse = connectDirection === 1 || connectDirection === 3;
ea.addArrow(newPoints,{
startArrowHead: reverse ? lines[1].endArrowhead : lines[1].startArrowhead,
endArrowHead: reverse ? lines[1].startArrowhead : lines[1].endArrowhead
});
break;
case 3: //line - line
ea.addLine(newPoints);
break;
}
ea.addElementsToView();

View File

@@ -0,0 +1,17 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.75819749055177 80.03703336574608" width="72.75819749055177" height="80.03703336574608">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(4 4) rotate(0 12.71901889991409 17.183109917454658)"><path d="M0 0 C0 7.02, 0 14.05, 0 34.37 M0 34.37 C7.62 34.37, 15.24 34.37, 25.44 34.37" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(51.379765518092086 61.93633577499986) rotate(0 5.684341886080802e-14 7.050348795373111)"><path d="M0 0 C0 4.06, 0 8.11, 0 14.1" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(34.0013341989918 20.987787610339183) rotate(0 17.378431645779926 17.378431645779983)"><path d="M34.76 17.38 C34.76 18.38, 34.67 19.41, 34.49 20.4 C34.32 21.39, 34.05 22.38, 33.71 23.32 C33.36 24.27, 32.93 25.2, 32.43 26.07 C31.93 26.94, 31.34 27.78, 30.69 28.55 C30.04 29.32, 29.32 30.04, 28.55 30.69 C27.78 31.34, 26.94 31.93, 26.07 32.43 C25.2 32.93, 24.27 33.36, 23.32 33.71 C22.38 34.05, 21.39 34.32, 20.4 34.49 C19.41 34.67, 18.38 34.76, 17.38 34.76 C16.37 34.76, 15.35 34.67, 14.36 34.49 C13.37 34.32, 12.38 34.05, 11.43 33.71 C10.49 33.36, 9.56 32.93, 8.69 32.43 C7.82 31.93, 6.98 31.34, 6.21 30.69 C5.44 30.04, 4.71 29.32, 4.07 28.55 C3.42 27.78, 2.83 26.94, 2.33 26.07 C1.83 25.2, 1.39 24.27, 1.05 23.32 C0.7 22.38, 0.44 21.39, 0.26 20.4 C0.09 19.41, 0 18.38, 0 17.38 C0 16.37, 0.09 15.35, 0.26 14.36 C0.44 13.37, 0.7 12.38, 1.05 11.43 C1.39 10.49, 1.83 9.56, 2.33 8.69 C2.83 7.82, 3.42 6.98, 4.07 6.21 C4.71 5.44, 5.44 4.71, 6.21 4.07 C6.98 3.42, 7.82 2.83, 8.69 2.33 C9.56 1.83, 10.49 1.39, 11.43 1.05 C12.38 0.7, 13.37 0.44, 14.36 0.26 C15.35 0.09, 16.37 0, 17.38 0 C18.38 0, 19.41 0.09, 20.4 0.26 C21.39 0.44, 22.38 0.7, 23.32 1.05 C24.27 1.39, 25.2 1.83, 26.07 2.33 C26.94 2.83, 27.78 3.42, 28.55 4.07 C29.32 4.71, 30.04 5.44, 30.69 6.21 C31.34 6.98, 31.93 7.82, 32.43 8.69 C32.93 9.56, 33.36 10.49, 33.71 11.43 C34.05 12.38, 34.32 13.37, 34.49 14.36 C34.67 15.35, 34.71 16.88, 34.76 17.38 C34.8 17.88, 34.8 16.88, 34.76 17.38" stroke="black" stroke-width="4" fill="none"></path></g><g stroke-linecap="round"><g transform="translate(41.72257566145686 38.36621939788711) rotate(0 9.65718949485347 0)"><path d="M0 0 C4.11 0, 8.22 0, 19.31 0 M0 0 C6.95 0, 13.9 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(41.72257587602678 38.36622004108449) rotate(89.99999999999994 9.65718949485347 0)"><path d="M0 0 C5.31 0, 10.62 0, 19.31 0 M0 0 C4.56 0, 9.13 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,75 @@
/*
Adds a rounded mask to the image by adding a full cover black mask and a rounded rectangle white mask. The script is also useful for adding just a black mask. In this case, run the script, then delete the white mask and add your custom white mask.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-crop-vintage.jpg)
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.18")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
if(!ea.isExcalidrawMaskFile()) {
new Notice("This script only works with Mask Files");
return;
}
const frames = ea.getViewElements().filter(el=>el.type==="frame")
if(frames.length !== 1) {
new Notice("Multiple frames found");
return;
}
const frame = frames[0];
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.frameId === frame.id));
const frameId = ea.generateElementId();
ea.style.fillStyle = "solid";
ea.style.roughness = 0;
ea.style.strokeColor = "transparent";
ea.style.strokeWidth = 0.1;
ea.style.opacity = 50;
let blackEl = ea.getViewElements().find(el=>el.id === "allblack");
let whiteEl = ea.getViewElements().find(el=>el.id === "whiteovr");
if(blackEl && whiteEl) {
ea.copyViewElementsToEAforEditing([blackEl, whiteEl]);
} else
if (blackEl && !whiteEl) {
ea.copyViewElementsToEAforEditing([blackEl]);
ea.style.backgroundColor = "white";
ea.addRect(frame.x,frame.y,frame.width,frame.height, "whiteovr");
} else
if (!blackEl && whiteEl) {
ea.style.backgroundColor = "black";
ea.addRect(frame.x-2,frame.y-2,frame.width+4,frame.height+4, "allblack");
ea.copyViewElementsToEAforEditing([whiteEl]);
} else {
ea.style.backgroundColor = "black";
ea.addRect(frame.x-2,frame.y-2,frame.width+4,frame.height+4, "allblack");
ea.style.backgroundColor = "white";
ea.addRect(frame.x,frame.y,frame.width,frame.height, "whiteovr");
}
blackEl = ea.getElement("allblack");
whiteEl = ea.getElement("whiteovr");
//this "magic" is required to ensure the frame element is above in sequence of the new rectangle elements
ea.getElements().forEach(el=>{el.frameId = frameId});
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === frame.id));
const newFrame = ea.getElement(frame.id);
newFrame.id = frameId;
ea.elementsDict[frameId] = newFrame;
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === frame.id));
ea.getElement(frame.id).isDeleted = true;
let curve = await utils.inputPrompt(
"Set roundess",
"Positive whole number",
`${whiteEl.roundness?.value ?? "500"}`
);
if(!curve) return;
curve = parseInt(curve);
if(isNaN(curve) || curve < 0) {
new Notice ("Roudness is not a valid positive whole number");
return;
}
whiteEl.roundness = {type: 3, value: curve};
ea.addElementsToView(false,false,true);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-squircle"><path fill="none" d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9"/></svg>

After

Width:  |  Height:  |  Size: 294 B

15
ea-scripts/Custom Zoom.md Normal file
View File

@@ -0,0 +1,15 @@
/*
You can set a custom zoom level with this script. This allows you to set a zoom level below 10% or set the zoom level to a specific value. Note however, that Excalidraw has a bug under 10% zoom, and a phantom copy of your image may appear on screen. If this happens, increase the zoom and the phantom should disappear, if it doesn't then close and open the drawing.
```js*/
const api = ea.getExcalidrawAPI();
const appState = api.getAppState();
const zoomStr = await utils.inputPrompt("Zoom [%]",null,`${appState.zoom.value*100}%`);
if(!zoomStr) return;
const zoomNum = parseFloat(zoomStr.match(/^\d*/)[0]);
if(isNaN(zoomNum)) {
new Notice("You must provide a number");
return;
}
ea.getExcalidrawAPI().updateScene({appState:{zoom:{value: zoomNum/100 }}});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-search"><g stroke-width="2"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/></g></svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@@ -3,13 +3,70 @@
Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.
<iframe width="560" height="315" src="https://www.youtube.com/embed/HRtaaD34Zzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/mvMQcz401yo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.29")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
// -------------------------------
// Utility variables and functions
// -------------------------------
const excalidrawTemplates = ea.getListOfTemplateFiles();
if(typeof window.ExcalidrawDeconstructElements === "undefined") {
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplates?.[0].path??""
};
}
const splitFolderAndFilename = (filepath) => {
const lastIndex = filepath.lastIndexOf("/");
return {
foldername: ea.obsidian.normalizePath(filepath.substring(0, lastIndex)),
filename: (lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1)) + ".md"
};
}
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Templates"]) {
settings = {
"Templates" : {
value: "",
description: "Comma-separated list of template filepaths"
}
};
await ea.setScriptSettings(settings);
}
if(!settings["Default file name"]) {
settings["Default file name"] = {
value: "deconstructed",
description: "The default filename to use when deconstructing elements."
};
await ea.setScriptSettings(settings);
}
const DEFAULT_FILENAME = settings["Default file name"].value;
const templates = settings["Templates"]
.value
.split(",")
.map(p=>app.metadataCache.getFirstLinkpathDest(p.trim(),""))
.concat(excalidrawTemplates)
.filter(f=>Boolean(f))
.sort((a,b) => a.basename.localeCompare(b.basename));
// ------------------------------------
// Prepare elements to be deconstructed
// ------------------------------------
const els = ea.getViewSelectedElements();
if (els.length === 0) {
new Notice("You must select elements first")
@@ -20,53 +77,126 @@ const bb = ea.getBoundingBox(els);
ea.copyViewElementsToEAforEditing(els);
ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
});
let folder = ea.targetView.file.path;
folder = folder.lastIndexOf("/")===-1?"":folder.substring(0,folder.lastIndexOf("/"))+"/";
const fname = await utils.inputPrompt("Filename for new file","Filename","");
const template = app.metadataCache.getFirstLinkpathDest(ea.plugin.settings.templateFilePath,"");
// ------------
// Input prompt
// ------------
let shouldAnchor = false;
const actionButtons = [
{
caption: "Insert @100%",
tooltip: "Anchor to 100% size",
action: () => {
shouldAnchor = true;
}
},
{
caption: "Insert",
tooltip: "Insert without anchoring",
action: () => {
shouldAnchor = false;
}
}];
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Select template`)
.addDropdown(dropdown => {
templates.forEach(file => dropdown.addOption(file.path, file.basename));
if(templates.length === 0) dropdown.addOption(null, "none");
dropdown
.setValue(window.ExcalidrawDeconstructElements.templatePath)
.onChange(value => {
window.ExcalidrawDeconstructElements.templatePath = value;
})
})
new ea.obsidian.Setting(container)
.setName(`Open deconstructed image`)
.addToggle((toggle) => toggle
.setValue(window.ExcalidrawDeconstructElements.openDeconstructedImage)
.onChange(value => {
window.ExcalidrawDeconstructElements.openDeconstructedImage = value;
})
)
}
const path = await utils.inputPrompt(
"Filename for new file",
"Filename",
await ea.getAttachmentFilepath(DEFAULT_FILENAME),
actionButtons,
2,
false,
customControls
);
if(!path) return;
// ----------------------
// Execute deconstruction
// ----------------------
const {foldername, filename} = splitFolderAndFilename(path);
const newPath = await ea.create ({
filename: fname + ".md",
foldername: folder,
templatePath: template?.path,
onNewPane: true
filename,
foldername,
templatePath: window.ExcalidrawDeconstructElements.templatePath,
onNewPane: true,
silent: !window.ExcalidrawDeconstructElements.openDeconstructedImage
});
setTimeout(async ()=>{
const file = app.metadataCache.getFirstLinkpathDest(newPath,"")
ea.deleteViewElements(els);
ea.clear();
await ea.addImage(bb.topX,bb.topY,file,false);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().history.clear(); //to avoid undo/redo messing up the decomposition
},1000);
let f = app.vault.getAbstractFileByPath(newPath);
let counter = 0;
while((!f || !ea.isExcalidrawFile(f)) && counter++<100) {
await sleep(50);
f = app.vault.getAbstractFileByPath(newPath);
}
if(!f || !ea.isExcalidrawFile(f)) {
new Notice("Something went wrong");
return;
}
let padding = parseFloat(app.metadataCache.getCache(f.path)?.frontmatter["excalidraw-export-padding"]);
if(isNaN(padding)) {
padding = ea.plugin.settings.exportPaddingSVG;
}
ea.getElements().forEach(el=>el.isDeleted = true);
await ea.addImage(bb.topX-padding,bb.topY-padding,f,false, shouldAnchor);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().history.clear();
if(!window.ExcalidrawDeconstructElements.openDeconstructedImage) {
new Notice("Deconstruction ready");
}

689
ea-scripts/ExcaliAI.md Normal file
View File

@@ -0,0 +1,689 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg)
```js*/
let dirty=false;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const outputTypes = {
"html": {
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
blocktype: "html"
},
"mermaid": {
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
blocktype: "mermaid"
},
"svg": {
instruction: "Return a single message containing only the SVG code in an html codeblock.",
blocktype: "svg"
},
"image-gen": {
instruction: "Return a single message with the generated image prompt in a codeblock",
blocktype: "image"
},
"image-gen-silent": {
instruction: "Return a single message with the generated image prompt in a codeblock",
blocktype: "image-silent"
},
"image-edit": {
instruction: "",
blocktype: "image"
}
}
const systemPrompts = {
"Challenge my thinking": {
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
type: "mermaid",
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
},
"Convert sketch to shapes": {
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
},
"Create a simple Excalidraw icon": {
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
},
"Create a stick figure": {
prompt: "You will receive a prompt from the user. Your task involves drawing a simple stick figure or a scene involving a few stick figures based on the user's prompt. Create the stickfigure based on the following style description. DO NOT add any detail, just use it AS-IS: Create a simple stick figure character with a large round head and a face in the style of sketchy caricatures. The stick figure should have a rudimentary body composed of straight lines representing the arms and legs. Hands and toes should be should be represented with round shapes, do not add details such as fingers or toes. Use fine lines, smooth curves, rounded shapes. The stick figure should retain a playful and childlike simplicity, reminiscent of a doodle someone might draw on the corner of a notebook page. Create a black and white drawing, a hand-drawn figure on white background.",
type: "image-gen",
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
},
"Edit an image": {
prompt: null,
type: "image-edit",
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
},
"Generate an image from image and prompt": {
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
type: "image-gen",
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
},
"Generate an image from prompt": {
prompt: null,
type: "image-gen",
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
},
"Generate an image to illustrate a quote": {
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
type: "image-gen",
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
},
"Generate 4 icon-variants based on input image": {
prompt: "Given a simple sketch and an optional text prompt from the user, your task is to generate a descriptive narrative tailored for effective image generation, capturing the style of the sketch. Utilize the text prompt to guide the description. Your objective is to instruct DALL-E to create a collage of four minimalist black and white hand-drawn pencil sketches in a 2x2 matrix format. Each sketch should convert the user's sketch into simple artistic SVG icons with transparent backgrounds. Ensure the resulting images remain text-free, maintaining a minimalist, easy-to-understand style, and omit framing borders. Only include a pencil in the drawing if it is explicitely metioned in the user prompt or included in the sketch.",
type: "image-gen-silent",
help: "Generate a collage of 4 icons based on the drawing using ChatGPT-Vision and Dall-e. You may provide a contextual text-prompt to improve accuracy of interpretation."
},
"Visual brainstorm": {
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
type: "image-gen",
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
},
"Wireframe to code": {
prompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
type: "html",
help: "Use GPT Visions to interpret the wireframe and generate a web application. YOu may copy the resulting code from the active embeddable's top left menu."
},
}
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Agent's Task"]) {
settings = {
"Agent's Task": "Wireframe to code",
"User Prompt": "",
};
await ea.setScriptSettings(settings);
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let userPrompt = settings["User Prompt"] ?? "";
let agentTask = settings["Agent's Task"];
let imageSize = settings["Image Size"]??"1024x1024";
if(!systemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(systemPrompts)[0];
}
let imageModel, valideSizes;
const setImageModelAndSizes = () => {
imageModel = systemPrompts[agentTask].type === "image-edit"
? "dall-e-2"
: ea.plugin.settings.openAIDefaultImageGenerationModel;
validSizes = imageModel === "dall-e-2"
? [`256x256`, `512x512`, `1024x1024`]
: (imageModel === "dall-e-3"
? [`1024x1024`, `1792x1024`, `1024x1792`]
: [`1024x1024`])
if(!validSizes.includes(imageSize)) {
imageSize = "1024x1024";
dirty = true;
}
}
setImageModelAndSizes();
// --------------------------------------
// Generate Image Blob From Selected Excalidraw Elements
// --------------------------------------
const calculateImageScale = (elements) => {
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
return minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
}
const createMask = async (dataURL) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// If opaque (alpha > 0), make it transparent
if (data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
} else if (data[i + 3] === 0) {
// If fully transparent, make it red
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // make it opaque
}
}
ctx.putImageData(imageData, 0, 0);
const maskDataURL = canvas.toDataURL();
resolve(maskDataURL);
};
img.onerror = error => {
reject(error);
};
img.src = dataURL;
});
}
//https://platform.openai.com/docs/api-reference/images/createEdit
//dall-e-2 image edit only works on square images
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
let squareBB;
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
let PADDING = 5;
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
return {imageDataURL: null, maskDataURL: null} ;
}
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
let maskDataURL;
const loader = ea.getEmbeddedFilesLoader(false);
let scale = calculateImageScale(ea.getElements());
const bb = ea.getBoundingBox(viewElements);
if(ea.getElements()
.filter(el=>el.type==="image")
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
) { PADDING = 0; }
let exportSettings = {withBackground: true, withTheme: true};
if(targetDalleImageEdit) {
PADDING = 0;
const strokeColor = ea.style.strokeColor;
const backgroundColor = ea.style.backgroundColor;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "transparent";
let rectID;
if(bb.height > bb.width) {
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
}
if(bb.width > bb.height) {
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
}
if(bb.height === bb.width) {
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
}
const rect = ea.getElement(rectID);
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
dalleWidth = parseInt(imageSize.split("x")[0]);
scale = dalleWidth/squareBB.width;
exportSettings = {withBackground: false, withTheme: true};
maskDataURL= await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
maskDataURL = await createMask(maskDataURL)
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
}
const imageDataURL = await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
ea.clear();
return {imageDataURL, maskDataURL};
}
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
// --------------------------------------
// Support functions - embeddable spinner and error
// --------------------------------------
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const errorMessage = async (spinnerID, message) => {
const error = "Something went wrong! Check developer console for more.";
const details = message ? `<p>${message}</p>` : "";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>${details}
</body></html>`);
new Notice (error);
ea.getElement(spinnerID).link = errorDataURL;
ea.addElementsToView(false,true);
}
// --------------------------------------
// Utility to write Mermaid to dialog
// --------------------------------------
const EDITOR_LS_KEYS = {
OAI_API_KEY: "excalidraw-oai-api-key",
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
PUBLISH_LIBRARY: "publish-library-data",
};
const setMermaidDataToStorage = (mermaidDefinition) => {
try {
window.localStorage.setItem(
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
JSON.stringify(mermaidDefinition)
);
return true;
} catch (error) {
console.warn(`localStorage.setItem error: ${error.message}`);
return false;
}
};
// --------------------------------------
// Submit Prompt
// --------------------------------------
const generateImage = async(text, spinnerID, bb, silent=false) => {
const requestObject = {
text,
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
},
};
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
const imageEl = ea.getElement(imageID);
const revisedPrompt = result.json.data[0].revised_prompt;
if(revisedPrompt && !silent) {
ea.style.fontSize = 16;
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
width: imageEl.width-30,
textAlign: "center",
textVerticalAlign: "top",
box: true,
})
ea.getElement(rectID).strokeColor = "transparent";
ea.getElement(rectID).backgroundColor = "transparent";
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
}
await ea.addElementsToView(false, true, true);
if(silent) return;
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
}
const run = async (text) => {
if(!text && !imageDataURL) {
new Notice("No prompt, aborting");
return;
}
const systemPrompt = systemPrompts[agentTask];
const outputType = outputTypes[systemPrompt.type];
const isImageGenRequest = outputType.blocktype === "image" || outputType.blocktype === "image-silent";
const isImageEditRequest = systemPrompt.type === "image-edit";
if(isImageEditRequest) {
if(!text) {
new Notice("You must provide a text prompt with instructions for how the image should be modified");
return;
}
if(!imageDataURL || !maskDataURL) {
new Notice("You must provide an image and a mask");
return;
}
}
//place spinner next to selected elements
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
//goes black (not freezes, but does not get a new frame for some reason)
//palcing this in an async call solves this issue
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
let isEACompleted = false;
setTimeout(async()=>{
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
isEACompleted = true;
});
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
generateImage(text,spinnerID,bb);
return;
}
const requestObject = isImageEditRequest
? {
...imageDataURL ? {image: {url: imageDataURL}} : {},
...(text && text.trim() !== "") ? {text} : {},
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
mask: maskDataURL,
},
}
: {
...imageDataURL ? {image: {url: imageDataURL}} : {},
...(text && text.trim() !== "") ? {text} : {},
systemPrompt: systemPrompt.prompt,
instruction: outputType.instruction,
}
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
//checking that EA has completed. Because the postOpenAI call is an async await
//I don't expect EA not to be completed by now. However the devil never sleeps.
//This (the insomnia of the Devil) is why I have a watchdog here as well
let counter = 0
while(!isEACompleted && counter++<10) sleep(50);
if(!isEACompleted) {
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
return;
}
if(isImageEditRequest) {
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
return;
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
//exctract codeblock and display result
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage(spinnerID);
return;
}
if(isImageGenRequest) {
generateImage(content,spinnerID,bb,outputType.blocktype === "image-silent");
return;
}
switch(outputType.blocktype) {
case "html":
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "svg":
ea.getElement(spinnerID).isDeleted = true;
ea.importSVG(content);
ea.addToGroup(ea.getElements().map(el=>el.id));
if(ea.getViewSelectedElements().length>0) {
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
}
ea.addElementsToView(true, false);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
try {
result = await ea.addMermaid(content);
if(typeof result === "string") {
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
return;
}
} catch (e) {
ea.addText(0,0,content);
}
ea.getElement(spinnerID).isDeleted = true;
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
await ea.addElementsToView(true, false);
setMermaidDataToStorage(content);
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
break;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
let previewDiv;
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-gen-silent" || systemPrompts[agentTask].type === "image-edit";
const addPreviewImage = () => {
if(!previewDiv) return;
previewDiv.empty();
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: imageDataURL,
}
});
if(maskDataURL) {
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: maskDataURL,
}
});
}
}
const configModal = new ea.obsidian.Modal(app);
configModal.modalEl.style.width="100%";
configModal.modalEl.style.maxWidth="1000px";
configModal.onOpen = async () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
new ea.obsidian.Setting(contentEl)
.setName("What would you like to do?")
.addDropdown(dropdown=>{
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(async (value) => {
dirty = true;
const prevTask = agentTask;
agentTask = value;
if(
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
) {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
addPreviewImage();
setImageModelAndSizes();
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
imageSizeSettingDropdown.setValue(imageSize);
}
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
const prompt = systemPrompts[value].prompt;
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
if(prompt) {
systemPromptDiv.style.display = "";
systemPromptTextArea.setValue(systemPrompts[value].prompt);
} else {
systemPromptDiv.style.display = "none";
}
});
})
helpEl = contentEl.createEl("p");
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
systemPromptDiv = contentEl.createDiv();
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
.addTextArea(text => {
systemPromptTextArea = text;
const prompt = systemPrompts[agentTask].prompt;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(prompt);
text.onChange(value => {
systemPrompts[value].prompt = value;
});
if(!prompt) systemPromptDiv.style.display = "none";
})
systemPromptSetting.nameEl.style.display = "none";
systemPromptSetting.descEl.style.display = "none";
systemPromptSetting.infoEl.style.display = "none";
contentEl.createEl("h4", {text: "User Prompt"});
const userPromptSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(userPrompt);
text.onChange(value => {
userPrompt = value;
dirty = true;
})
})
userPromptSetting.nameEl.style.display = "none";
userPromptSetting.descEl.style.display = "none";
userPromptSetting.infoEl.style.display = "none";
imageSizeSetting = new ea.obsidian.Setting(contentEl)
.setName("Select image size")
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
.addDropdown(dropdown=>{
validSizes.forEach(size=>dropdown.addOption(size,size));
imageSizeSettingDropdown = dropdown;
dropdown
.setValue(imageSize)
.onChange(async (value) => {
dirty = true;
imageSize = value;
if(systemPrompts[agentTask].type === "image-edit") {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
addPreviewImage();
}
});
})
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
if(imageDataURL) {
previewDiv = contentEl.createDiv({
attr: {
style: "text-align: center;",
}
});
addPreviewImage();
} else {
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
}
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["User Prompt"] = userPrompt;
settings["Agent's Task"] = agentTask;
settings["Image Size"] = imageSize;
ea.setScriptSettings(settings);
}
}
configModal.open();

1
ea-scripts/ExcaliAI.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,259 @@
/*
Generates a hierarchical Markdown document out of a visual layout of an article.
Watch this video to understand how the script is intended to work:
![Excalidraw Writing Machine YouTube Video](https://youtu.be/zvRpCOZAUSs)
You can download the sample Obsidian Templater file from [here](https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9)
You can download the demo PDF document showcased in the video from [here](https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf)
```js*/
const selectedElements = ea.getViewSelectedElements();
if (selectedElements.length !== 1 || selectedElements[0].type === "arrow") {
new Notice("Select a single element that is not an arrow and not a frame");
return;
}
const visited = new Set(); // Avoiding recursive infinite loops
delete window.ewm;
await ea.targetView.save();
//------------------
// Load Settings
//------------------
let settings = ea.getScriptSettings();
//set default values on first run
let didSettingsChange = false;
if(!settings["Template path"]) {
settings = {
"Template path" : {
value: "",
description: "The template file path that will receive the concatenated text. If the file includes <<<REPLACE ME>>> then it will be replaced with the generated text, if <<<REPLACE ME>>> is not present in the file the hierarchical markdown generated from the diagram will be added to the end of the template."
},
"ZK '# Summary' section": {
value: "Summary",
description: "The section in your visual zettelkasten file that contains the short written summary of the idea. This is the text that will be included in the hierarchical markdown file if visual ZK cards are included in your flow"
},
"ZK '# Source' section": {
value: "Source",
description: "The section in your visual zettelkasten file that contains the reference to your source. If present in the file, this text will be included in the output file as a reference"
},
"Embed image links": {
value: true,
description: "Should the resulting markdown document include the ![[embedded images]]?"
}
};
didSettingsChange = true;
}
if(!settings["Generate ![markdown](links)"]) {
settings["Generate ![markdown](links)"] = {
value: true,
description: "If you turn this off the script will generate ![[wikilinks]] for images"
}
didSettingsChange = true;
}
if(didSettingsChange) {
await ea.setScriptSettings(settings);
}
const ZK_SOURCE = settings["ZK '# Source' section"].value;
const ZK_SECTION = settings["ZK '# Summary' section"].value;
const INCLUDE_IMG_LINK = settings["Embed image links"].value;
const MARKDOWN_LINKS = settings["Generate ![markdown](links)"].value;
let templatePath = settings["Template path"].value;
//------------------
// Select template file
//------------------
const MSG = "Select another file"
let selection = MSG;
if(templatePath && app.vault.getAbstractFileByPath(templatePath)) {
selection = await utils.suggester([templatePath, MSG],[templatePath, MSG], "Use previous template or select another?");
if(!selection) {
new Notice("process aborted");
return;
}
}
if(selection === MSG) {
const files = app.vault.getMarkdownFiles().map(f=>f.path);
selection = await utils.suggester(files,files,"Select the template to use. ESC to not use a tempalte");
}
if(selection && selection !== templatePath) {
settings["Template path"].value = selection;
await ea.setScriptSettings(settings);
}
templatePath = selection;
//------------------
// supporting functions
//------------------
function getNextElementFollowingArrow(el, arrow) {
if (arrow.startBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.endBinding?.elementId);
}
if (arrow.endBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.startBinding?.elementId);
}
return null;
}
function getImageLink(f) {
if(MARKDOWN_LINKS) {
return `![${f.basename}](${encodeURI(f.path)})`;
}
return `![[${f.path}|${f.basename}]]`;
}
function getBoundText(el) {
const textId = el.boundElements?.find(x => x.type === "text")?.id;
const text = ea.getViewElements().find(x => x.id === textId)?.originalText;
return text ? text + "\n" : "";
}
async function getSectionText(file, section) {
const content = await app.vault.cachedRead(file);
const metadata = app.metadataCache.getFileCache(file);
if (!metadata || !metadata.headings) {
return null;
}
const targetHeading = metadata.headings.find(h => h.heading === section);
if (!targetHeading) {
return null;
}
const startPos = targetHeading.position.start.offset;
let endPos = content.length;
const nextHeading = metadata.headings.find(h => h.position.start.offset > startPos);
if (nextHeading) {
endPos = nextHeading.position.start.offset;
}
let sectionContent = content.slice(startPos, endPos).trim();
sectionContent = sectionContent.substring(sectionContent.indexOf('\n') + 1).trim();
// Remove Markdown comments enclosed in %%
sectionContent = sectionContent.replace(/%%[\s\S]*?%%/g, '').trim();
return sectionContent;
}
async function getBlockText(file, blockref) {
const content = await app.vault.cachedRead(file);
const blockPattern = new RegExp(`\\^${blockref}\\b`, 'g');
let blockPosition = content.search(blockPattern);
if (blockPosition === -1) {
return "";
}
const startPos = content.lastIndexOf('\n', blockPosition) + 1;
let endPos = content.indexOf('\n', blockPosition);
if (endPos === -1) {
endPos = content.length;
} else {
const nextBlockOrHeading = content.slice(endPos).search(/(^# |^\^|\n)/gm);
if (nextBlockOrHeading !== -1) {
endPos += nextBlockOrHeading;
} else {
endPos = content.length;
}
}
let blockContent = content.slice(startPos, endPos).trim();
blockContent = blockContent.replace(blockPattern, '').trim();
blockContent = blockContent.replace(/%%[\s\S]*?%%/g, '').trim();
return blockContent;
}
async function getElementText(el) {
if (el.type === "text") {
return el.originalText;
}
if (el.type === "image") {
const f = ea.getViewFileForImageElement(el);
if(!ea.isExcalidrawFile(f)) return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
let source = await getSectionText(f, ZK_SOURCE);
source = source ? ` (source:: ${source})` : "";
const summary = await getSectionText(f, ZK_SECTION) ;
if(summary) return (INCLUDE_IMG_LINK ? `${getImageLink(f)}\n${summary + source}` : summary + source) + "\n";
return f.name + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
}
if (el.type === "embeddable") {
const linkWithRef = el.link.match(/\[\[([^\]]*)]]/)?.[1];
if(!linkWithRef) return "";
const path = linkWithRef.split("#")[0];
const f = app.metadataCache.getFirstLinkpathDest(path, ea.targetView.file.path);
if(!f) return "";
if(f.extension !== "md") return f.name;
const ref = linkWithRef.split("#")[1];
if(!ref) return await app.vault.read(f);
if(ref.startsWith("^")) {
return await getBlockText(f, ref.substring(1));
} else {
return await getSectionText(f, ref);
}
}
return getBoundText(el);
}
//------------------
// Navigating the hierarchy
//------------------
async function crawl(el, level, isFirst = false) {
visited.add(el.id);
let result = await getElementText(el) + "\n";
// Process all arrows connected to this element
const boundElementsData = el.boundElements.filter(x => x.type === "arrow");
const isFork = boundElementsData.length > (isFirst ? 1 : 2);
if(isFork) level++;
for(const bindingData of boundElementsData) {
const arrow = ea.getViewElements().find(x=> x.id === bindingData.id);
const nextEl = getNextElementFollowingArrow(el, arrow);
if (nextEl && !visited.has(nextEl.id)) {
if(isFork) result += `\n${"#".repeat(level)} `;
const arrowLabel = getBoundText(arrow);
if (arrowLabel) {
// If the arrow has a label, add it as an additional level
result += arrowLabel + "\n";
result += await crawl(nextEl, level);
} else {
// If no label, continue to the next element
result += await crawl(nextEl, level);
}
}
};
return result;
}
window.ewm = "## " + await crawl(selectedElements[0], 2, true);
const outputPath = await ea.getAttachmentFilepath(`EWM - ${ea.targetView.file.name}.md`);
let result = templatePath
? await app.vault.read(app.vault.getAbstractFileByPath(templatePath))
: "";
if(result.match("<<<REPLACE ME>>>")) {
result = result.replaceAll("<<<REPLACE ME>>>",window.ewm);
} else {
result += window.ewm;
}
const outfile = await app.vault.create(outputPath,result);
setTimeout(()=>{
ea.openFileInNewOrAdjacentLeaf(outfile);
}, 250);

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="CurrentColor" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard">
<path stroke-width="2" d="M10 8h.01"/>
<path stroke-width="2" d="M12 12h.01"/>
<path stroke-width="2" d="M14 8h.01"/>
<path stroke-width="2" d="M16 12h.01"/>
<path stroke-width="2" d="M18 8h.01"/>
<path stroke-width="2" d="M6 8h.01"/>
<path stroke-width="2" d="M7 16h10"/>
<path stroke-width="2" d="M8 12h.01"/>
<path fill="none" stroke-width="2" d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

673
ea-scripts/GPT-Draw-a-UI.md Normal file
View File

@@ -0,0 +1,673 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg)
```js*/
let dirty=false;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.12")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const outputTypes = {
"html": {
instruction: "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
blocktype: "html"
},
"mermaid": {
instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
blocktype: "mermaid"
},
"svg": {
instruction: "Return a single message containing only the SVG code in an html codeblock.",
blocktype: "svg"
},
"image-gen": {
instruction: "Return a single message with the generated image prompt in a codeblock",
blocktype: "image"
},
"image-edit": {
instruction: "",
blocktype: "image"
}
}
const systemPrompts = {
"Challenge my thinking": {
prompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
type: "mermaid",
help: "Translate your image and optional text prompt into a Mermaid mindmap. If there are conversion errors, edit the Mermaid script under 'More Tools'."
},
"Convert sketch to shapes": {
prompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert selected scribbles into shapes; works better with fewer shapes. Experimental and may not produce good drawings."
},
"Create a simple Excalidraw icon": {
prompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
type: "svg",
help: "Convert text prompts into simple icons inserted as Excalidraw elements. Expect only a text prompt. Experimental and may not produce good drawings."
},
"Edit an image": {
prompt: null,
type: "image-edit",
help: "Image elements will be used as the Image. Shapes on top of the image will be the Mask. Use the prompt to instruct Dall-e about the changes. Dall-e-2 model will be used."
},
"Generate an image from image and prompt": {
prompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
type: "image-gen",
help: "Generate an image based on the drawing and prompt using ChatGPT-Vision and Dall-e. Provide a contextual text-prompt for accurate interpretation."
},
"Generate an image from prompt": {
prompt: null,
type: "image-gen",
help: "Send only the text prompt to OpenAI. Provide a detailed description; OpenAI will enrich your prompt automatically. To avoid it, start your prompt like this 'DO NOT add any detail, just use it AS-IS:'"
},
"Generate an image to illustrate a quote": {
prompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
type: "image-gen",
help: "ExcaliAI will create an image prompt to illustrate your text input - a quote - with GPT, then generate an image using Dall-e. In case you include the Author's name, GPT will try to generate an image that in some way references the Author."
},
"Visual brainstorm": {
prompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
type: "image-gen",
help: "Use ChatGPT Visions and Dall-e to create an image based on your text prompt and image to spark new ideas."
},
"Wireframe to code": {
prompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
type: "html",
help: "Use GPT Visions to interpret the wireframe and generate a web application. You may copy the resulting code from the active embeddable's top left menu."
},
}
const IMAGE_WARNING = "The generated image is linked through a temporary OpenAI URL and will be removed in approximately 30 minutes. To save it permanently, choose 'Save image from URL to local file' from the Obsidian Command Palette."
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Agent's Task"]) {
settings = {
"Agent's Task": "Wireframe to code",
"User Prompt": "",
};
await ea.setScriptSettings(settings);
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let userPrompt = settings["User Prompt"] ?? "";
let agentTask = settings["Agent's Task"];
let imageSize = settings["Image Size"]??"1024x1024";
if(!systemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(systemPrompts)[0];
}
let imageModel, valideSizes;
const setImageModelAndSizes = () => {
imageModel = systemPrompts[agentTask].type === "image-edit"
? "dall-e-2"
: ea.plugin.settings.openAIDefaultImageGenerationModel;
validSizes = imageModel === "dall-e-2"
? [`256x256`, `512x512`, `1024x1024`]
: (imageModel === "dall-e-3"
? [`1024x1024`, `1792x1024`, `1024x1792`]
: [`1024x1024`])
if(!validSizes.includes(imageSize)) {
imageSize = "1024x1024";
dirty = true;
}
}
setImageModelAndSizes();
// --------------------------------------
// Generate Image Blob From Selected Excalidraw Elements
// --------------------------------------
const calculateImageScale = (elements) => {
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
return minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
}
const createMask = async (dataURL) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// If opaque (alpha > 0), make it transparent
if (data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
} else if (data[i + 3] === 0) {
// If fully transparent, make it red
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // make it opaque
}
}
ctx.putImageData(imageData, 0, 0);
const maskDataURL = canvas.toDataURL();
resolve(maskDataURL);
};
img.onerror = error => {
reject(error);
};
img.src = dataURL;
});
}
//https://platform.openai.com/docs/api-reference/images/createEdit
//dall-e-2 image edit only works on square images
//if targetDalleImageEdit === true then the image and the mask will be returned in two separate dataURLs
let squareBB;
const generateCanvasDataURL = async (view, targetDalleImageEdit=false) => {
let PADDING = 5;
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
return {imageDataURL: null, maskDataURL: null} ;
}
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
let maskDataURL;
const loader = ea.getEmbeddedFilesLoader(false);
let scale = calculateImageScale(ea.getElements());
const bb = ea.getBoundingBox(viewElements);
if(ea.getElements()
.filter(el=>el.type==="image")
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
) { PADDING = 0; }
let exportSettings = {withBackground: true, withTheme: true};
if(targetDalleImageEdit) {
PADDING = 0;
const strokeColor = ea.style.strokeColor;
const backgroundColor = ea.style.backgroundColor;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "transparent";
let rectID;
if(bb.height > bb.width) {
rectID = ea.addRect(bb.topX-(bb.height-bb.width)/2, bb.topY,bb.height, bb.height);
}
if(bb.width > bb.height) {
rectID = ea.addRect(bb.topX, bb.topY-(bb.width-bb.height)/2,bb.width, bb.width);
}
if(bb.height === bb.width) {
rectID = ea.addRect(bb.topX, bb.topY, bb.width, bb.height);
}
const rect = ea.getElement(rectID);
squareBB = {topX: rect.x-PADDING, topY: rect.y-PADDING, width: rect.width + 2*PADDING, height: rect.height + 2*PADDING};
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
dalleWidth = parseInt(imageSize.split("x")[0]);
scale = dalleWidth/squareBB.width;
exportSettings = {withBackground: false, withTheme: true};
maskDataURL= await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
maskDataURL = await createMask(maskDataURL)
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
}
const imageDataURL = await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
ea.clear();
return {imageDataURL, maskDataURL};
}
let {imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[agentTask].type === "image-edit");
// --------------------------------------
// Support functions - embeddable spinner and error
// --------------------------------------
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {
0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;}
50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;}
100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}
}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const errorMessage = async (spinnerID, message) => {
const error = "Something went wrong! Check developer console for more.";
const details = message ? `<p>${message}</p>` : "";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>${details}
</body></html>`);
new Notice (error);
ea.getElement(spinnerID).link = errorDataURL;
ea.addElementsToView(false,true);
}
// --------------------------------------
// Utility to write Mermaid to dialog
// --------------------------------------
const EDITOR_LS_KEYS = {
OAI_API_KEY: "excalidraw-oai-api-key",
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
PUBLISH_LIBRARY: "publish-library-data",
};
const setMermaidDataToStorage = (mermaidDefinition) => {
try {
window.localStorage.setItem(
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
JSON.stringify(mermaidDefinition)
);
return true;
} catch (error) {
console.warn(`localStorage.setItem error: ${error.message}`);
return false;
}
};
// --------------------------------------
// Submit Prompt
// --------------------------------------
const generateImage = async(text, spinnerID, bb) => {
const requestObject = {
text,
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
},
};
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
const imageEl = ea.getElement(imageID);
const revisedPrompt = result.json.data[0].revised_prompt;
if(revisedPrompt) {
ea.style.fontSize = 16;
const rectID = ea.addText(imageEl.x+15, imageEl.y + imageEl.height + 50, revisedPrompt, {
width: imageEl.width-30,
textAlign: "center",
textVerticalAlign: "top",
box: true,
})
ea.getElement(rectID).strokeColor = "transparent";
ea.getElement(rectID).backgroundColor = "transparent";
ea.addToGroup(ea.getElements().filter(el=>el.id !== spinnerID).map(el=>el.id));
}
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
}
const run = async (text) => {
if(!text && !imageDataURL) {
new Notice("No prompt, aborting");
return;
}
const systemPrompt = systemPrompts[agentTask];
const outputType = outputTypes[systemPrompt.type];
const isImageGenRequest = outputType.blocktype === "image";
const isImageEditRequest = systemPrompt.type === "image-edit";
if(isImageEditRequest) {
if(!text) {
new Notice("You must provide a text prompt with instructions for how the image should be modified");
return;
}
if(!imageDataURL || !maskDataURL) {
new Notice("You must provide an image and a mask");
return;
}
}
//place spinner next to selected elements
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const spinnerID = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
//this block is in an async call using the isEACompleted flag because otherwise during debug Obsidian
//goes black (not freezes, but does not get a new frame for some reason)
//palcing this in an async call solves this issue
//If you know why this is happening and can offer a better solution, please reach out to @zsviczian
let isEACompleted = false;
setTimeout(async()=>{
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===spinnerID);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
isEACompleted = true;
});
if(isImageGenRequest && !systemPrompt.prompt && !isImageEditRequest) {
generateImage(text,spinnerID,bb);
return;
}
const requestObject = isImageEditRequest
? {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
imageGenerationProperties: {
size: imageSize,
//quality: "standard", //not supported by dall-e-2
n:1,
mask: maskDataURL,
},
}
: {
...imageDataURL ? {image: imageDataURL} : {},
...(text && text.trim() !== "") ? {text} : {},
systemPrompt: systemPrompt.prompt,
instruction: outputType.instruction,
}
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
console.log({result, json:result?.json});
//checking that EA has completed. Because the postOpenAI call is an async await
//I don't expect EA not to be completed by now. However the devil never sleeps.
//This (the insomnia of the Devil) is why I have a watchdog here as well
let counter = 0
while(!isEACompleted && counter++<10) sleep(50);
if(!isEACompleted) {
await errorMessage(spinnerID, "Unexpected issue with ExcalidrawAutomate");
return;
}
if(isImageEditRequest) {
if(!result?.json?.data?.[0]?.url) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
const spinner = ea.getElement(spinnerID)
spinner.isDeleted = true;
const imageID = await ea.addImage(spinner.x, spinner.y, result.json.data[0].url);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().setToast({
message: IMAGE_WARNING,
duration: 15000,
closable: true
});
return;
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage(spinnerID, result?.json?.error?.message);
return;
}
//extract codeblock and display result
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage(spinnerID);
return;
}
if(isImageGenRequest) {
generateImage(content,spinnerID,bb);
return;
}
switch(outputType.blocktype) {
case "html":
ea.getElement(spinnerID).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "svg":
ea.getElement(spinnerID).isDeleted = true;
ea.importSVG(content);
ea.addToGroup(ea.getElements().map(el=>el.id));
if(ea.getViewSelectedElements().length>0) {
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY};
}
ea.addElementsToView(true, false);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
try {
result = await ea.addMermaid(content);
if(typeof result === "string") {
await errorMessage(spinnerID, "Open [More Tools / Mermaid to Excalidraw] to manually fix the received mermaid script<br><br>" + result);
return;
}
} catch (e) {
ea.addText(0,0,content);
}
ea.getElement(spinnerID).isDeleted = true;
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height};
await ea.addElementsToView(true, false);
setMermaidDataToStorage(content);
new Notice("Open More Tools/Mermaid to Excalidraw in the top tools menu to edit the generated diagram",8000);
break;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
let previewDiv;
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const isImageGenerationTask = () => systemPrompts[agentTask].type === "image-gen" || systemPrompts[agentTask].type === "image-edit";
const addPreviewImage = () => {
if(!previewDiv) return;
previewDiv.empty();
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: imageDataURL,
}
});
if(maskDataURL) {
previewDiv.createEl("img",{
attr: {
style: `max-width: 100%;max-height: 30vh;`,
src: maskDataURL,
}
});
}
}
const configModal = new ea.obsidian.Modal(app);
configModal.modalEl.style.width="100%";
configModal.modalEl.style.maxWidth="1000px";
configModal.onOpen = async () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let systemPromptTextArea, systemPromptDiv, imageSizeSetting, imageSizeSettingDropdown, helpEl;
new ea.obsidian.Setting(contentEl)
.setName("What would you like to do?")
.addDropdown(dropdown=>{
Object.keys(systemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(async (value) => {
dirty = true;
const prevTask = agentTask;
agentTask = value;
if(
(systemPrompts[prevTask].type === "image-edit" && systemPrompts[value].type !== "image-edit") ||
(systemPrompts[prevTask].type !== "image-edit" && systemPrompts[value].type === "image-edit")
) {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, systemPrompts[value].type === "image-edit"));
addPreviewImage();
setImageModelAndSizes();
while (imageSizeSettingDropdown.selectEl.options.length > 0) { imageSizeSettingDropdown.selectEl.remove(0); }
validSizes.forEach(size=>imageSizeSettingDropdown.addOption(size,size));
imageSizeSettingDropdown.setValue(imageSize);
}
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
const prompt = systemPrompts[value].prompt;
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[value].help;
if(prompt) {
systemPromptDiv.style.display = "";
systemPromptTextArea.setValue(systemPrompts[value].prompt);
} else {
systemPromptDiv.style.display = "none";
}
});
})
helpEl = contentEl.createEl("p");
helpEl.innerHTML = `<b>Help: </b>` + systemPrompts[agentTask].help;
systemPromptDiv = contentEl.createDiv();
systemPromptDiv.createEl("h4", {text: "Customize System Prompt"});
systemPromptDiv.createEl("span", {text: "Unless you know what you are doing I do not recommend changing the system prompt"})
const systemPromptSetting = new ea.obsidian.Setting(systemPromptDiv)
.addTextArea(text => {
systemPromptTextArea = text;
const prompt = systemPrompts[agentTask].prompt;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(prompt);
text.onChange(value => {
systemPrompts[value].prompt = value;
});
if(!prompt) systemPromptDiv.style.display = "none";
})
systemPromptSetting.nameEl.style.display = "none";
systemPromptSetting.descEl.style.display = "none";
systemPromptSetting.infoEl.style.display = "none";
contentEl.createEl("h4", {text: "User Prompt"});
const userPromptSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
text.inputEl.style.minHeight = "10em";
text.inputEl.style.width = "100%";
text.setValue(userPrompt);
text.onChange(value => {
userPrompt = value;
dirty = true;
})
})
userPromptSetting.nameEl.style.display = "none";
userPromptSetting.descEl.style.display = "none";
userPromptSetting.infoEl.style.display = "none";
imageSizeSetting = new ea.obsidian.Setting(contentEl)
.setName("Select image size")
.setDesc(fragWithHTML("<mark>⚠️ Important ⚠️</mark>: " + IMAGE_WARNING))
.addDropdown(dropdown=>{
validSizes.forEach(size=>dropdown.addOption(size,size));
imageSizeSettingDropdown = dropdown;
dropdown
.setValue(imageSize)
.onChange(async (value) => {
dirty = true;
imageSize = value;
if(systemPrompts[agentTask].type === "image-edit") {
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, true));
addPreviewImage();
}
});
})
imageSizeSetting.settingEl.style.display = isImageGenerationTask() ? "" : "none";
if(imageDataURL) {
previewDiv = contentEl.createDiv({
attr: {
style: "text-align: center;",
}
});
addPreviewImage();
} else {
contentEl.createEl("h4", {text: "No elements are selected from your canvas"});
contentEl.createEl("span", {text: "Because there are no Excalidraw elements selected on the canvas, only the text prompt will be sent to OpenAI."});
}
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
run(userPrompt); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["User Prompt"] = userPrompt;
settings["Agent's Task"] = agentTask;
settings["Image Size"] = imageSize;
ea.setScriptSettings(settings);
}
}
configModal.open();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>

After

Width:  |  Height:  |  Size: 694 B

673
ea-scripts/Golden Ratio.md Normal file
View File

@@ -0,0 +1,673 @@
/*
The script performs two different functions depending on the elements selected in the view.
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/golden-ratio.jpg)
<iframe width="560" height="315" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Gravitational point of spiral: $$\left[x,y\right]=\left[ x + \frac{{\text{width} \cdot \phi^2}}{{\phi^2 + 1}}\;, \; y + \frac{{\text{height} \cdot \phi^2}}{{\phi^2 + 1}} \right]$$
Dimensions of inner rectangles in case of Double Spiral: $$[width, height] = \left[\frac{width\cdot(\phi^2+1)}{2\phi^2}\;, \;\frac{height\cdot(\phi^2+1)}{2\phi^2}\right]$$
```js*/
const phi = (1 + Math.sqrt(5)) / 2; // Golden Ratio (φ)
const inversePhi = (1-1/phi);
const pointsPerCurve = 20; // Number of points per curve segment
const ownerWindow = ea.targetView.ownerWindow;
const hostLeaf = ea.targetView.leaf;
let dirty = false;
const ids = [];
const textEls = ea.getViewSelectedElements().filter(el=>el.type === "text");
let rect = ea.getViewSelectedElements().length === 1 ? ea.getViewSelectedElement() : null;
if(!rect || rect.type !== "rectangle") {
//Fontsize cycle
if(textEls.length>0) {
if(window.excalidrawGoldenRatio) {
clearTimeout(window.excalidrawGoldenRatio?.timer);
} else {
window.excalidrawGoldenRatio = {timer: null, cycle:-1};
}
window.excalidrawGoldenRatio.timer = setTimeout(()=>{delete window.excalidrawGoldenRatio;},2000);
window.excalidrawGoldenRatio.cycle = (window.excalidrawGoldenRatio.cycle+1)%5;
ea.copyViewElementsToEAforEditing(textEls);
ea.getElements().forEach(el=> {
el.fontSize = window.excalidrawGoldenRatio.cycle === 2
? el.fontSize / Math.pow(phi,4)
: el.fontSize * phi;
const font = ExcalidrawLib.getFontString(el);
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
el.width = width;
el.height = height;
el.baseline = baseline;
});
ea.addElementsToView();
return;
}
new Notice("Select text elements, or a select a single rectangle");
return;
}
ea.copyViewElementsToEAforEditing([rect]);
rect = ea.getElement(rect.id);
ea.style.strokeColor = rect.strokeColor;
ea.style.strokeWidth = rect.strokeWidth;
ea.style.roughness = rect.roughness;
ea.style.angle = rect.angle;
let {x,y,width,height} = rect;
// --------------------------------------------
// Load Settings
// --------------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Horizontal Grid"]) {
settings = {
"Horizontal Grid" : {
value: "left-right",
valueset: ["none","letf-right","right-left","center-out","center-in"]
},
"Vertical Grid": {
value: "none",
valueset: ["none","top-down","bottom-up","center-out","center-in"]
},
"Size": {
value: "6",
valueset: ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
},
"Aspect Choice": {
value: "none",
valueset: ["none","adjust-width","adjust-height"]
},
"Type": "grid",
"Spiral Orientation": {
value: "top-left",
valueset: ["double","top-left","top-right","bottom-right","bottom-left"]
},
"Lock Elements": false,
"Send to Back": false,
"Update Style": false,
"Bold Spiral": false,
};
await ea.setScriptSettings(settings);
}
let hDirection = settings["Horizontal Grid"].value;
let vDirection = settings["Vertical Grid"].value;
let aspectChoice = settings["Aspect Choice"].value;
let type = settings["Type"];
let spiralOrientation = settings["Spiral Orientation"].value;
let lockElements = settings["Lock Elements"];
let sendToBack = settings["Send to Back"];
let size = parseInt(settings["Size"].value);
let updateStyle = settings["Update Style"];
let boldSpiral = settings["Bold Spiral"];
// --------------------------------------------
// Rotation
// --------------------------------------------
let centerX, centerY;
const rotatePointAndAddToElementList = (elementID) => {
ids.push(elementID);
const line = ea.getElement(elementID);
// Calculate the initial position of the line's center
const lineCenterX = line.x + line.width / 2;
const lineCenterY = line.y + line.height / 2;
// Calculate the difference between the line's center and the rectangle's center
const diffX = lineCenterX - (rect.x + rect.width / 2);
const diffY = lineCenterY - (rect.y + rect.height / 2);
// Apply the rotation to the difference
const cosTheta = Math.cos(rect.angle);
const sinTheta = Math.sin(rect.angle);
const rotatedX = diffX * cosTheta - diffY * sinTheta;
const rotatedY = diffX * sinTheta + diffY * cosTheta;
// Calculate the new position of the line's center with respect to the rectangle's center
const newLineCenterX = rotatedX + (rect.x + rect.width / 2);
const newLineCenterY = rotatedY + (rect.y + rect.height / 2);
// Update the line's coordinates by adjusting for the change in the center
line.x += newLineCenterX - lineCenterX;
line.y += newLineCenterY - lineCenterY;
}
const rotatePointsWithinRectangle = (points) => {
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
const cosTheta = Math.cos(rect.angle);
const sinTheta = Math.sin(rect.angle);
const rotatedPoints = points.map(([x, y]) => {
// Translate the point relative to the rectangle's center
const translatedX = x - centerX;
const translatedY = y - centerY;
// Apply the rotation to the translated coordinates
const rotatedX = translatedX * cosTheta - translatedY * sinTheta;
const rotatedY = translatedX * sinTheta + translatedY * cosTheta;
// Translate back to the original coordinate system
const finalX = rotatedX + centerX;
const finalY = rotatedY + centerY;
return [finalX, finalY];
});
return rotatedPoints;
}
// --------------------------------------------
// Grid
// --------------------------------------------
const calculateGoldenSum = (baseOfGoldenGrid, pow) => {
const ratio = 1 / phi;
const geometricSum = baseOfGoldenGrid * ((1 - Math.pow(ratio, pow)) / (1 - ratio));
return geometricSum;
};
const findBaseForGoldenGrid = (targetValue, n, scenario) => {
const ratio = 1 / phi;
if (scenario === "center-out") {
return targetValue * (2-2*ratio) / (1 + ratio + 2*Math.pow(ratio,n));
} else if (scenario === "center-in") {
return targetValue*2*(1-ratio)*Math.pow(phi,n-1) /(2*Math.pow(phi,n-1)*(1-Math.pow(ratio,n))-1+ratio);
} else {
return targetValue * (1-ratio)/(1-Math.pow(ratio,n));
}
}
const calculateOffsetVertical = (scenario, base) => {
if (scenario === "center-out") return base / 2;
if (scenario === "center-in") return base / Math.pow(phi, size + 1) / 2;
return 0;
};
const horizontal = (direction, scenario) => {
const base = findBaseForGoldenGrid(width, size + 1, scenario);
const totalGridWidth = calculateGoldenSum(base, size + 1);
for (i = 1; i <= size; i++) {
const offset =
scenario === "center-out"
? totalGridWidth - calculateGoldenSum(base, i)
: calculateGoldenSum(base, size + 1 - i);
const x2 =
direction === "left"
? x + offset
: x + width - offset;
rotatePointAndAddToElementList(
ea.addLine([
[x2, y],
[x2, y + height],
])
);
}
};
const vertical = (direction, scenario) => {
const base = findBaseForGoldenGrid(height, size + 1, scenario);
const totalGridWidth = calculateGoldenSum(base, size + 1);
for (i = 1; i <= size; i++) {
const offset =
scenario === "center-out"
? totalGridWidth - calculateGoldenSum(base, i)
: calculateGoldenSum(base, size + 1 - i);
const y2 =
direction === "top"
? y + offset
: y + height - offset;
rotatePointAndAddToElementList(
ea.addLine([
[x, y2],
[x+width, y2],
])
);
}
};
const centerHorizontal = (scenario) => {
width = width / 2;
horizontal("left", scenario);
x += width;
horizontal("right", scenario);
x -= width;
width = 2*width;
};
const centerVertical = (scenario) => {
height = height / 2;
vertical("top", scenario);
y += height;
vertical("bottom", scenario);
y -= height;
height = 2*height;
};
const drawGrid = () => {
switch(hDirection) {
case "none": break;
case "left-right": horizontal("left"); break;
case "right-left": horizontal("right"); break;
case "center-out": centerHorizontal("center-out"); break;
case "center-in": centerHorizontal("center-in"); break;
}
switch(vDirection) {
case "none": break;
case "top-down": vertical("top"); break;
case "bottom-up": vertical("bottom"); break;
case "center-out": centerVertical("center-out"); break;
case "center-in": centerVertical("center-in"); break;
}
}
// --------------------------------------------
// Draw Spiral
// --------------------------------------------
const drawSpiral = () => {
let nextX, nextY, nextW, nextH;
let spiralPoints = [];
let curveEndX, curveEndY, curveX, curveY;
const phaseShift = {
"bottom-right": 0,
"bottom-left": 2,
"top-left": 2,
"top-right": 0,
}[spiralOrientation];
let curveStartX = {
"bottom-right": x,
"bottom-left": x+width,
"top-left": x+width,
"top-right": x,
}[spiralOrientation];
let curveStartY = {
"bottom-right": y+height,
"bottom-left": y+height,
"top-left": y,
"top-right": y,
}[spiralOrientation];
const mirror = spiralOrientation === "bottom-left" || spiralOrientation === "top-right";
for (let i = phaseShift; i < size+phaseShift; i++) {
const curvePhase = i%4;
const linePhase = mirror?[0,3,2,1][curvePhase]:curvePhase;
const longHorizontal = width/phi;
const shortHorizontal = width*inversePhi;
const longVertical = height/phi;
const shortVertical = height*inversePhi;
switch(linePhase) {
case 0: //right
nextX = x + longHorizontal;
nextY = y;
nextW = shortHorizontal;
nextH = height;
break;
case 1: //down
nextX = x;
nextY = y + longVertical;
nextW = width;
nextH = shortVertical;
break;
case 2: //left
nextX = x;
nextY = y;
nextW = shortHorizontal;
nextH = height;
break;
case 3: //up
nextX = x;
nextY = y;
nextW = width;
nextH = shortVertical;
break;
}
switch(curvePhase) {
case 0: //right
curveEndX = nextX;
curveEndY = mirror ? nextY + nextH : nextY;
break;
case 1: //down
curveEndX = nextX + nextW;
curveEndY = mirror ? nextY + nextH : nextY;
break;
case 2: //left
curveEndX = nextX + nextW;
curveEndY = mirror ? nextY : nextY + nextH;
break;
case 3: //up
curveEndX = nextX;
curveEndY = mirror ? nextY : nextY + nextH;
break;
}
// Add points for the curve segment
for (let j = 0; j <= pointsPerCurve; j++) {
const t = j / pointsPerCurve;
const angle = -Math.PI / 2 * t;
switch(curvePhase) {
case 0:
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
break;
case 1:
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
break;
case 2:
curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle);
curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle);
break;
case 3:
curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle);
curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle);
break;
}
spiralPoints.push([curveX, curveY]);
}
x = nextX;
y = nextY;
curveStartX = curveEndX;
curveStartY = curveEndY;
width = nextW;
height = nextH;
switch(linePhase) {
case 0: rotatePointAndAddToElementList(ea.addLine([[x,y],[x,y+height]]));break;
case 1: rotatePointAndAddToElementList(ea.addLine([[x,y],[x+width,y]]));break;
case 2: rotatePointAndAddToElementList(ea.addLine([[x+width,y],[x+width,y+height]]));break;
case 3: rotatePointAndAddToElementList(ea.addLine([[x,y+height],[x+width,y+height]]));break;
}
}
const strokeWidth = ea.style.strokeWidth;
ea.style.strokeWidth = strokeWidth * (boldSpiral ? 3 : 1);
const angle = ea.style.angle;
ea.style.angle = 0;
ids.push(ea.addLine(rotatePointsWithinRectangle(spiralPoints)));
ea.style.angle = angle;
ea.style.strokeWidth = strokeWidth;
}
// --------------------------------------------
// Update Aspect Ratio
// --------------------------------------------
const updateAspectRatio = () => {
switch(aspectChoice) {
case "none": break;
case "adjust-width": rect.width = rect.height/phi; break;
case "adjust-height": rect.height = rect.width/phi; break;
}
({x,y,width,height} = rect);
centerX = x + width/2;
centerY = y + height/2;
}
// --------------------------------------------
// UI
// --------------------------------------------
draw = async () => {
if(updateStyle) {
ea.style.strokeWidth = 0.5; rect.strokeWidth;
ea.style.roughness = 0; rect.roughness;
ea.style.roundness = null;
rect.strokeWidth = 0.5;
rect.roughness = 0;
rect.roundness = null;
}
updateAspectRatio();
switch(type) {
case "grid": drawGrid(); break;
case "spiral":
if(spiralOrientation === "double") {
wInner = width * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
hInner = height * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2));
x2 = width - wInner + x;
y2 = height - hInner + y;
width = wInner;
height = hInner;
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
spiralOrientation = "bottom-right";
drawSpiral();
x = x2;
y = y2;
width = wInner;
height = hInner;
rotatePointAndAddToElementList(ea.addRect(x,y,width,height));
spiralOrientation = "top-left";
drawSpiral();
spiralOrientation = "double";
} else {
drawSpiral();
}
break;
}
ea.addToGroup(ids);
ids.push(rect.id);
ea.addToGroup(ids);
lockElements && ea.getElements().forEach(el=>{el.locked = true;});
await ea.addElementsToView(false,false,!sendToBack);
!lockElements && ea.selectElementsInView(ea.getViewElements().filter(el => ids.includes(el.id)));
}
const modal = new ea.obsidian.Modal(app);
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const keydownListener = (e) => {
if(hostLeaf !== app.workspace.activeLeaf) return;
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
if(e.key === "Enter" && (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey)) {
e.preventDefault();
modal.close();
draw()
}
}
ownerWindow.addEventListener('keydown',keydownListener);
modal.onOpen = async () => {
const contentEl = modal.contentEl;
contentEl.createEl("h1", {text: "Golden Ratio"});
new ea.obsidian.Setting(contentEl)
.setName("Adjust Rectangle Aspect Ratio to Golden Ratio")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("adjust-width","Adjust Width")
.addOption("adjust-height","Adjust Height")
.setValue(aspectChoice)
.onChange(value => {
aspectChoice = value;
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName("Change Line Style To: thin, architect, sharp")
.addToggle(toggle=>
toggle
.setValue(updateStyle)
.onChange(value => {
dirty = true;
updateStyle = value;
})
)
let sizeEl;
new ea.obsidian.Setting(contentEl)
.setName("Number of lines")
.addSlider(slider => slider
.setLimits(2, 20, 1)
.setValue(size)
.onChange(value => {
sizeEl.innerText = ` ${value.toString()}`;
size = value;
dirty = true;
}),
)
.settingEl.createDiv("", el => {
sizeEl = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${size.toString()}`;
});
new ea.obsidian.Setting(contentEl)
.setName("Lock Rectangle and Gridlines")
.addToggle(toggle=>
toggle
.setValue(lockElements)
.onChange(value => {
dirty = true;
lockElements = value;
})
)
new ea.obsidian.Setting(contentEl)
.setName("Send to Back")
.addToggle(toggle=>
toggle
.setValue(sendToBack)
.onChange(value => {
dirty = true;
sendToBack = value;
})
)
let bGrid, bSpiral;
let sHGrid, sVGrid, sSpiral, sBoldSpiral;
const showGridSettings = (value) => {
value
? (bGrid.setCta(), bSpiral.removeCta())
: (bGrid.removeCta(), bSpiral.setCta());
sHGrid.settingEl.style.display = value ? "" : "none";
sVGrid.settingEl.style.display = value ? "" : "none";
sSpiral.settingEl.style.display = !value ? "" : "none";
sBoldSpiral.settingEl.style.display = !value ? "" : "none";
}
new ea.obsidian.Setting(contentEl)
.setName(fragWithHTML("<h3>Output Type</h3>"))
.addButton(button => {
bGrid = button;
button
.setButtonText("Grid")
.setCta(type === "grid")
.onClick(event => {
type = "grid";
showGridSettings(true);
dirty = true;
})
})
.addButton(button => {
bSpiral = button;
button
.setButtonText("Spiral")
.setCta(type === "spiral")
.onClick(event => {
type = "spiral";
showGridSettings(false);
dirty = true;
})
});
sSpiral = new ea.obsidian.Setting(contentEl)
.setName("Spiral Orientation")
.addDropdown(dropdown=>dropdown
.addOption("double","Double")
.addOption("top-left","Top left")
.addOption("top-right","Top right")
.addOption("bottom-right","Bottom right")
.addOption("bottom-left","Bottom left")
.setValue(spiralOrientation)
.onChange(value => {
spiralOrientation = value;
dirty = true;
})
);
sBoldSpiral = new ea.obsidian.Setting(contentEl)
.setName("Spiral with Bold Line")
.addToggle(toggle=>
toggle
.setValue(boldSpiral)
.onChange(value => {
dirty = true;
boldSpiral = value;
})
)
sHGrid = new ea.obsidian.Setting(contentEl)
.setName("Horizontal Grid")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("left-right","Left to right")
.addOption("right-left","Right to left")
.addOption("center-out","Center out")
.addOption("center-in","Center in")
.setValue(hDirection)
.onChange(value => {
hDirection = value;
dirty = true;
})
);
sVGrid = new ea.obsidian.Setting(contentEl)
.setName("Vertical Grid")
.addDropdown(dropdown=>dropdown
.addOption("none","None")
.addOption("top-down","Top down")
.addOption("bottom-up","Bottom up")
.addOption("center-out","Center out")
.addOption("center-in","Center in")
.setValue(vDirection)
.onChange(value => {
vDirection = value;
dirty = true;
})
);
showGridSettings(type === "grid");
new ea.obsidian.Setting(contentEl)
.addButton(button => button
.setButtonText("Run")
.setCta(true)
.onClick(async (event) => {
draw();
modal.close();
})
);
}
modal.onClose = () => {
if(dirty) {
settings["Horizontal Grid"].value = hDirection;
settings["Vertical Grid"].value = vDirection;
settings["Size"].value = size.toString();
settings["Aspect Choice"].value = aspectChoice;
settings["Type"] = type;
settings["Spiral Orientation"].value = spiralOrientation;
settings["Lock Elements"] = lockElements;
settings["Send to Back"] = sendToBack;
settings["Update Style"] = updateStyle;
settings["Bold Spiral"] = boldSpiral;
ea.setScriptSettings(settings);
}
ownerWindow.removeEventListener('keydown',keydownListener);
}
modal.open();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 642.8 373.7" stroke="currentColor"><path stroke-linecap="round" stroke-width="4" d="M5 5h633M5 5h633m0 0v364m0-364v364m0 0H5m633 0H5m0 0V5m0 364V5m242 0v364m0-364v364M5 144h242M5 144h242M154 5v139m0-139v139m0-53h93m-93 0h93m-57 0v53m0-53v53m-36-33h36m-36 0h36m-14-20v20m0-20v20m0-8h14m-14 0h14"/><path stroke-linecap="round" stroke-width="12" d="m638 5-5 57m5-57-5 57m0 0-14 55m14-55-14 55m0 0-24 53m24-53-24 53m0 0-32 49m32-49-32 49m0 0-40 43m40-43-40 43m0 0-46 37m46-37-46 37m0 0-53 30m53-30-53 30m0 0-56 22m56-22-56 22m0 0-60 13m60-13-60 13m0 0-61 5m61-5-61 5m0 0s0 0 0 0m0 0s0 0 0 0m0 0-38-3m38 3-38-3m0 0-37-8m37 8-37-8m0 0-35-14m35 14-35-14m0 0-32-18m32 18-32-18m0 0-29-23m29 23-29-23m0 0-25-27m25 27-25-27m0 0-20-30m20 30-20-30m0 0-14-33m14 33-14-33m0 0-9-34m9 34-9-34m0 0-3-35m3 35-3-35m0 0s0 0 0 0m0 0s0 0 0 0m0 0 2-22m-2 22 2-22m0 0 5-21m-5 21 5-21m0 0 9-20m-9 20 9-20m0 0 13-19M21 81l13-19m0 0 15-16M34 62l15-16m0 0 18-14M49 46l18-14m0 0 20-12M67 32c4-3 8-6 20-12m0 0 21-8m-21 8 21-8m0 0 23-5m-23 5 23-5m0 0 23-2m-23 2 23-2m0 0s0 0 0 0m0 0s0 0 0 0m0 0 15 1m-15-1 15 1m0 0 14 3m-14-3 14 3m0 0 13 5m-13-5 13 5m0 0 13 7m-13-7 13 7m0 0 11 9m-11-9 11 9m0 0 9 10m-9-10 9 10m0 0 8 12m-8-12 8 12m0 0 5 12m-5-12 5 12m0 0 4 13m-4-13 4 13m0 0 1 14m-1-14 1 14m0 0s0 0 0 0m0 0s0 0 0 0m0 0-1 8m1-8-1 8m0 0-2 8m2-8-2 8m0 0-4 8m4-8-4 8m0 0-4 7m4-7-4 7m0 0-6 6m6-6-6 6m0 0-7 6m7-6-7 6m0 0-7 4m7-4-7 4m0 0-9 3m9-3-9 3m0 0-8 2m8-2-8 2m0 0-9 1m9-1-9 1m0 0s0 0 0 0m0 0s0 0 0 0m0 0h-6m6 0h-6m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-5-2m5 2-5-2m0 0-4-4m4 4-4-4m0 0-4-4m4 4-4-4m0 0-3-4m3 4-3-4m0 0-2-5m2 5-2-5m0 0-1-5m1 5-1-5m0 0-1-5m1 5-1-5m0 0s0 0 0 0m0 0s0 0 0 0m0 0 1-3m-1 3 1-3m0 0v-3m0 3v-3m0 0 2-3m-2 3 2-3m0 0 2-3m-2 3 2-3m0 0 2-2m-2 2 2-2m0 0 2-2m-2 2 2-2m0 0 3-2m-3 2 3-2m0 0 3-1m-3 1 3-1m0 0 4-1m-4 1 4-1m0 0h3m-3 0h3m0 0s0 0 0 0m0 0s0 0 0 0m0 0h2m-2 0h2m0 0h2m-2 0h2m0 0 2 1m-2-1 2 1m0 0 2 1m-2-1 2 1m0 0 2 2m-2-2 2 2m0 0 1 1m-1-1 1 1m0 0 1 2m-1-2 1 2m0 0 1 2m-1-2 1 2m0 0v1m0-1v1m0 0 1 2m-1-2 1 2"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -29,7 +29,7 @@ try {
elHeight /= 1.1;
}
} else if (elWidth * elHeight < areaAvailable) {
while (elWidth * elHeight > areaAvailable) {
while (elWidth * elHeight < areaAvailable) {
elWidth *= 1.1;
elHeight *= 1.1;
}
@@ -64,4 +64,4 @@ try {
ea.addElementsToView(false, true, true);
} catch (err) {
_ = new Notice(err.toString())
}
}

View File

@@ -33,8 +33,20 @@ const invertColor = (color) => {
}
}
const invertPaletteColors = (palette) => Object.keys(palette).forEach(key => palette[key] = invertColor(palette[key]));
Object.keys(colorPalette).forEach(key => invertPaletteColors(colorPalette[key]));
function invertColorsRecursively(obj) {
if (typeof obj === 'string') {
return invertColor(obj);
} else if (Array.isArray(obj)) {
return obj.map(item => invertColorsRecursively(item));
} else if (typeof obj === 'object' && obj !== null) {
const result = {};
Object.keys(obj).forEach(key => result[key] = invertColorsRecursively(obj[key]));
return result;
} else {
return obj;
}
}
colorPalette = invertColorsRecursively(colorPalette);
ea.copyViewElementsToEAforEditing(ea.getViewElements());
ea.getElements().forEach(el=>{

View File

@@ -33,6 +33,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Add Link to Existing File and Open](Add%20Link%20to%20Existing%20File%20and%20Open.md)|Prompts for a file from the vault. Adds a link to the selected element pointing to the selected file. You can control in settings to open the file in the current active pane or an adjacent pane.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-link-and-open.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Add Link to New Page and Open](Add%20Link%20and%20Open%20Page.md)|Prompts for filename. Offers option to create and open a new Markdown or Excalidraw document. Adds link pointing to the new file, to the selected objects in the drawing. You can control in settings to open the file in the current active pane or an adjacent pane.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-link-to-new-page-and-pen.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Add Next Step in Process](Add%20Link%20to%20New%20Page%20and%20Open.md)|This script will prompt you for the title of the process step, then will create a stick note with the text. If an element is selected then the script will connect this new step with an arrow to the previous step (the selected element). If no element is selected, then the script assumes this is the first step in the process and will only output the sticky note with the text that was entered.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-process-step.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split Ellipse](Boolean%20Operations.md)|With This Script it is possible to make boolean Operations on Shapes.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png)|[@GColoy](https://github.com/GColoy)|
|[Box Each Selected Groups](Box%20Each%20Selected%20Groups.md)|This script will add encapsulating boxes around each of the currently selected groups in Excalidraw.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-box-each-selected-groups.png)|[@1-2-3](https://github.com/1-2-3)|
|[Box Selected Elements](Box%20Selected%20Elements.md)|This script will add an encapsulating box around the currently selected elements in Excalidraw.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-box-elements.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Change shape of selected elements](Change%20shape%20of%20selected%20elements.md)|The script allows you to change the shape of selected Rectangles, Diamonds and Ellipses|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-change-shape.jpg)|[@zsviczian](https://github.com/zsviczian)|
@@ -57,7 +58,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Mindmap connector](Mindmap%20connector.md)|This script creates mindmap like lines (only right side and down available currently) for selected elements. The line will start according to the creation time of the elements. So you should create the header element first.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/mindmap%20connector.png)|[@xllowl](https://github.com/xllowl)|
|[Modify background color opacity](Modify%20background%20color%20opacity.md)|This script changes the opacity of the background color of the selected boxes. The default background color in Excalidraw is so dark that the text is hard to read. You can lighten the color a bit by setting transparency. And you can tweak the transparency over and over again until you're happy with it. Although excalidraw has the opacity option in its native property Settings, it also changes the transparency of the border. Use this script to change only the opacity of the background color without affecting the border.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-modify-background-color-opacity.png)|[@1-2-3](https://github.com/1-2-3)|
|[Normalize Selected Arrows](Normalize%20Selected%20Arrows.md)|This script will reset the start and end positions of the selected arrows. The arrow will point to the center of the connected box and will have a gap of 8px from the box.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-normalize-selected-arrows.png)|[@1-2-3](https://github.com/1-2-3)|
|[OCR - Optical Character Recognition](OCR%20-%20Optical%20Character%20Recognition.md)|The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to exctract the text from the image, and 2) will add the text to your drawing as a text element.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[OCR - Optical Character Recognition](OCR%20-%20Optical%20Character%20Recognition.md)|The script will 1) send the selected image file to [taskbone.com](https://taskbone.com) to extract the text from the image, and 2) will add the text to your drawing as a text element.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ocr.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Organic Line](Organic%20Line.md)|Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Repeat Elements](Repeat%20Elements.md)|This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-elements.png)|[@1-2-3](https://github.com/1-2-3)|
|[Reverse arrows](Reverse%20arrows.md)|Reverse the direction of **arrows** within the scope of selected elements.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reverse-arrow.jpg)|[@zsviczian](https://github.com/zsviczian)|
@@ -71,8 +72,10 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Set stroke width of selected elements](Set%20Stroke%20Width%20of%20Selected%20Elements.md)|This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split text by lines](Split%20text%20by%20lines.md)|Split lines of text into separate text elements for easier reorganization|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Set Text Alignment](Set%20Text%20Alignment.md)|Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split Ellipse](Split%20Ellipse.md)|This script splits an ellipse at any point where a line intersects it.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png)|[@GColoy](https://github.com/GColoy)|
|[TheBrain-navigation](TheBrain-navigation.md)|An Excalidraw based graph user interface for your Vault. Requires the [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview). Generates a graph view similar to that of [TheBrain](https://TheBrain.com) plex. Watch introduction to this script on [YouTube](https://youtu.be/plYobK-VufM).|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/TheBrain.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Fullscreen on Mobile](Toggle%20Fullscreen%20on%20Mobile.md)|Hides Obsidian workspace leaf padding and header (based on option in settings, default is "hide header" = false) which will take Excalidraw to full screen. ⚠ Note that if the header is not visible, it will be very difficult to invoke the command palette to end full screen. Only hide the header if you have a keyboard or you've practiced opening command palette!|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/ea-toggle-fullscreen.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Grid](Toggle%20Grid.md)|Toggles the grid.||[@GColoy](https://github.com/GColoy)|
|[Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md)|The script will delete the selected text elements from the canvas and will copy the text from these text elements into the Excalidraw markdown file as metadata. This means, that the text will no longer be visible in the drawing, however you will be able to search for the text in Obsidian and find the drawing containing this image.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-metadata.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Zoom to Fit Selected Elements](Zoom%20to%20Fit%20Selected%20Elements.md)|Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)||[@zsviczian](https://github.com/zsviczian)|
|[Hardware Eraser Suppoer](Hardware%20Eraser%20Support.md)|Allows the use of pen inversion/hardware erasers on supported pens.|[@threethan](https://github.com/threethan)|

View File

@@ -0,0 +1,40 @@
/*
The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.
```js*/
const FONTSIZES = [16, 20, 28, 36];
const api = ea.getExcalidrawAPI();
const st = api.getAppState();
const zoom = st.zoom.value;
const currentItemFontSize = st.currentItemFontSize;
const fontsizes = FONTSIZES.map(s=>s/zoom);
const els = ea.getViewSelectedElements().filter(el=>el.type === "text");
const findClosestIndex = (val, list) => {
let closestIndex = 0;
let closestDifference = Math.abs(list[0] - val);
for (let i = 1; i < list.length; i++) {
const difference = Math.abs(list[i] - val);
if (difference <= closestDifference) {
closestDifference = difference;
closestIndex = i;
}
}
return closestIndex;
}
ea.viewUpdateScene({appState:{currentItemFontSize: fontsizes[(findClosestIndex(currentItemFontSize, fontsizes)+1) % fontsizes.length] }});
if(els.length>0) {
ea.copyViewElementsToEAforEditing(els);
ea.getElements().forEach(el=> {
el.fontSize = fontsizes[(findClosestIndex(el.fontSize, fontsizes)+1) % fontsizes.length];
const font = ExcalidrawLib.getFontString(el);
const lineHeight = ExcalidrawLib.getDefaultLineHeight(el.fontFamily);
const {width, height, baseline} = ExcalidrawLib.measureText(el.originalText, font, lineHeight);
el.width = width;
el.height = height;
el.baseline = baseline;
});
ea.addElementsToView();
}

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 50 30" xmlns="http://www.w3.org/2000/svg">
<text fill="currentColor" x="10" y="30" font-size="16px" font-weight="light">A</text>
<text fill="currentColor" x="22" y="30" font-size="36px" font-weight="light">A</text>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -0,0 +1,96 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-texts.png)
In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers.
Inspired from [Repeat Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Elements.md)
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
let repeatNum = parseInt(await utils.inputPrompt("repeat times?","number","5"));
if(!repeatNum) {
new Notice("Please enter a number.");
return;
}
const selectedElements = ea.getViewSelectedElements().sort((lha,rha) => lha.x === rha.x ? lha.y - rha.y : lha.x - rha.x);
const selectedBounds = selectedElements.filter(e => e.type !== "text");
const selectedTexts = selectedElements.filter(e => e.type === "text");
const selectedTextsById = selectedTexts.reduce((prev, next) => (prev[next.id] = next, prev), {})
if(selectedTexts.length !== 2 || ![0, 2].includes(selectedBounds.length)) {
new Notice("Please select only 2 text elements.");
return;
}
if(selectedBounds.length === 2) {
if(selectedBounds[0].type !== selectedBounds[1].type) {
new Notice("The selected elements must be of the same type.");
return;
}
if (!selectedBounds.every(e => e.boundElements?.length === 1)) {
new Notice("Only support the bound element with 1 text element.");
return;
}
if (!selectedBounds.every(e => !!selectedTextsById[e.boundElements?.[0]?.id])) {
new Notice("Bound element must refer to the text element.");
return;
}
}
const prevBoundEl = selectedBounds.length ? selectedBounds[0] : selectedTexts[0];
const nextBoundEl = selectedBounds.length ? selectedBounds[1] : selectedTexts[1];
const prevTextEl = prevBoundEl.type === 'text' ? prevBoundEl : selectedTextsById[prevBoundEl.boundElements[0].id]
const nextTextEl = nextBoundEl.type === 'text' ? nextBoundEl : selectedTextsById[nextBoundEl.boundElements[0].id]
const xDistance = nextBoundEl.x - prevBoundEl.x;
const yDistance = nextBoundEl.y - prevBoundEl.y;
const numReg = /\d+/
let textNumDiff
try {
const num0 = +prevTextEl.text.match(numReg)
const num1 = +nextTextEl.text.match(numReg)
textNumDiff = num1 - num0
} catch(e) {
new Notice("Text must include a number!")
return;
}
const repeatEl = (newEl, step) => {
ea.elementsDict[newEl.id] = newEl;
newEl.x += xDistance * (step + 1);
newEl.y += yDistance * (step + 1);
if(newEl.text) {
const text = newEl.text.replace(numReg, (match) => +match + (step + 1) * textNumDiff)
newEl.originalText = text
newEl.rawText = text
newEl.text = text
}
}
ea.copyViewElementsToEAforEditing(selectedBounds);
for(let i=0; i<repeatNum; i++) {
const newTextEl = ea.cloneElement(nextTextEl);
repeatEl(newTextEl, i)
if (selectedBounds.length) {
const newBoundEl = ea.cloneElement(selectedBounds[1]);
newBoundEl.boundElements[0].id = newTextEl.id
newTextEl.containerId = newBoundEl.id
repeatEl(newBoundEl, i)
}
}
await ea.addElementsToView(false, false, true);

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g><g><path fill="#000000" d="M101.2,18.7c-2,0.9-38,36.6-39.2,39c-1,1.9-1.1,4.2-0.4,6.2c0.6,1.5,37.3,38.6,39.3,39.6c1.7,0.9,5.2,0.8,7.1-0.1c1.9-0.9,3.7-3.5,4.1-5.8c0.6-3.5-0.2-4.5-12.5-16.8L88.2,69.2h44.3c38.7,0,44.8,0.1,48.6,0.7c24.2,4.2,43.2,22.6,48.1,46.8c3.3,16.4-1,34.2-11.5,47.6c-3.5,4.6-4.1,6.9-2.4,10.4c1.8,3.7,6.6,5.3,10.4,3.5c2.8-1.3,8.7-9.4,12.5-17.2c10.5-20.8,10.5-45.2,0-66.2c-10.3-20.6-28.6-34.8-51.8-40c-4.6-1.1-4.8-1.1-51.3-1.2c-25.7-0.1-46.7-0.3-46.7-0.5s5.2-5.5,11.5-11.9c9.4-9.4,11.6-11.9,12-13.3C113.6,21.6,107.3,16.1,101.2,18.7z"/><path fill="#000000" d="M34.9,73.4c-1.8,0.5-5.8,4.4-9.7,9.6c-7.9,10.4-12.8,22.4-14.5,35.4c-5.3,40.1,22.9,77.3,63.1,83.3c4,0.6,11.3,0.7,45.4,0.7c24,0,40.6,0.2,40.6,0.4c0,0.2-5,5.3-11.1,11.1c-11.3,11-12.9,13-12.9,16c0,4.1,3.8,7.9,7.9,7.9c0.7,0,2.1-0.3,3.1-0.7c2.3-1,38.1-36.6,39.3-39.2c1-2.1,1.1-4.2,0.4-6.1c-0.6-1.5-37.3-38.6-39.3-39.7c-1.9-1-6.1-0.6-8.1,0.8c-2.9,2-4,6.4-2.5,9.4c0.4,0.8,5.9,6.6,12.1,12.9l11.4,11.4h-40.4c-35,0-40.9-0.1-44.7-0.7c-24.2-4.1-43.4-22.9-48.2-47.1c-1.1-5.5-1.1-16.4,0-21.9c2.1-10.7,7-20.2,14.6-28.7c2.1-2.3,3.5-4.3,3.8-5.4c1-3.6-0.7-7.3-4.2-9C38.7,72.8,37.4,72.7,34.9,73.4z"/></g></g></g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -2,12 +2,12 @@
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png)
This script allows users to streamline their Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. Users can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. Users can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.
This script enables the selection of elements based on matching properties. Select the attributes (such as stroke color, fill style, font family, etc) that should match for selection. It's perfect for large scenes where manual selection of elements would be cumbersome. You can either run the script to select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria to.
```js */
let config = window.ExcalidrawSelectConfig;
config = config && (Date.now() - config.timestamp < 60000) ? config : null;
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
@@ -19,7 +19,7 @@ if(!config && (elements.length !==1)) {
}
}
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead} = ea.getViewSelectedElement();
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead, fileId} = ea.getViewSelectedElement();
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
@@ -27,14 +27,14 @@ const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerH
// RUN
//--------------------------
const run = () => {
selectedElements = ea.getViewElements().filter(el=>
selectedElements = elements.filter(el=>
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
@@ -43,7 +43,8 @@ const run = () => {
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
((typeof config.type === "undefined") || (el.type === config.type)) &&
((typeof config.startArrowhead === "undefined") || (el.startArrowhead === config.startArrowhead)) &&
((typeof config.endArrowhead === "undefined") || (el.endArrowhead === config.endArrowhead))
((typeof config.endArrowhead === "undefined") || (el.endArrowhead === config.endArrowhead)) &&
((typeof config.fileId === "undefined") || (el.fileId === config.fileId))
)
ea.selectElementsInView(selectedElements);
delete window.ExcalidrawSelectConfig;
@@ -55,12 +56,12 @@ const run = () => {
const showInstructions = () => {
const instructionsModal = new ea.obsidian.Modal(app);
instructionsModal.onOpen = () => {
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
instructionsModal.contentEl.createEl("p", {text: "Step 1: Choose the attributes that you want the selected elements to match."});
instructionsModal.contentEl.createEl("p", {text: "Step 2: Select an action:"});
instructionsModal.contentEl.createEl("ul", {}, el => {
el.createEl("li", {text: "Click 'RUN' to find matching elements throughout the entire scene."});
el.createEl("li", {text: "Click 'SELECT' to first choose a specific group of elements. Then run the 'Select Similar Elements' script once more on that group within 1 minute."});
el.createEl("li", {text: "Click 'SELECT' to 1) first choose a specific group of elements in the scene, then 2) run the 'Select Similar Elements' once more within 1 minute to apply the filter criteria only to that group of elements."});
});
instructionsModal.contentEl.createEl("p", {text: "Note: If you choose 'SELECT', make sure to click the 'Select Similar Elements' script again within 1 minute to apply your selection criteria to the group of elements you chose."});
};
@@ -70,14 +71,14 @@ const showInstructions = () => {
const selectAttributesToCopy = () => {
const configModal = new ea.obsidian.Modal(app);
configModal.onOpen = () => {
config = {};
config = {};
configModal.contentEl.createEl("h1", {text: "Select Similar Elements"});
new ea.obsidian.Setting(configModal.contentEl)
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
.addButton(button => button
.setButtonText("Instructions")
.onClick(showInstructions)
);
new ea.obsidian.Setting(configModal.contentEl)
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
.addButton(button => button
.setButtonText("Instructions")
.onClick(showInstructions)
);
// Add Toggles for the rest of the attributes
@@ -97,11 +98,12 @@ const selectAttributesToCopy = () => {
{name: "End arrowhead", key: "endArrowhead"},
{name: "Height", key: "height"},
{name: "Width", key: "width"},
{name: "ImageID", key: "fileId"},
];
attributes.forEach(attr => {
const attrValue = elements[0][attr.key];
if(attrValue || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
if((typeof attrValue !== "undefined" && attrValue !== null) || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
let description = '';
switch(attr.key) {
@@ -142,8 +144,6 @@ const selectAttributesToCopy = () => {
description = `${attrValue}`;
break;
default:
console.log(attr.key);
console.log(attrValue);
description = `${attrValue.charAt(0).toUpperCase() + attrValue.slice(1)}`;
break;
}
@@ -165,7 +165,7 @@ const selectAttributesToCopy = () => {
});
//Add Toggle for the rest of the attirbutes. Organize attributes into a logical sequence or groups by adding
//Add Toggle for the rest of the attributes. Organize attributes into a logical sequence or groups by adding
//configModal.contentEl.createEl("h") or similar to the code
new ea.obsidian.Setting(configModal.contentEl)
@@ -190,7 +190,9 @@ const selectAttributesToCopy = () => {
configModal.onClose = () => {
setTimeout(()=>delete configModal);
setTimeout(()=>{
delete configModal
});
}
configModal.open();

View File

@@ -8,18 +8,84 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.11")) {
if(ea.verifyMinimumPluginVersion && ea.verifyMinimumPluginVersion("2.4.0")) {
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
let gridFrequency = appState.gridStep;;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const gridSize = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.GridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(gridSize)) return; //this is to avoid passing an illegal value to Excalidraw
const gridStep = isNaN(parseInt(gridFrequency)) ? appState.gridStep : parseInt(gridFrequency);
api.updateScene({
appState : {gridSize, gridStep, gridModeEnabled:true},
commitToHistory:false
});
}
// ----------------
// old script
// ----------------
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
const grid = parseInt(await utils.inputPrompt("Grid size?",null,appState.previousGridSize?.toString()??"20"));
const gridColor = appState.gridColor;
let gridFrequency = gridColor?.MajorGridFrequency ?? 5;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const grid = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.previousGridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(grid)) return; //this is to avoid passing an illegal value to Excalidraw
appState.gridSize = grid;
appState.previousGridSize = grid;
if(gridColor) gridColor.MajorGridFrequency = parseInt(gridFrequency);
api.updateScene({
appState,
appState : {gridSize: grid, previousGridSize: grid, gridColor},
commitToHistory:false
});

View File

@@ -9,7 +9,11 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
width = await utils.inputPrompt("Width?","number",width);
width = parseFloat(await utils.inputPrompt("Width?","number",width));
if(isNaN(width)) {
new Notice("Invalid number");
return;
}
const elements=ea.getViewSelectedElements();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>el.strokeWidth=width);

View File

@@ -1,24 +1,39 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
# About the slideshow script
The script will convert your drawing into a slideshow presentation.
![Slideshow 3.0](https://www.youtube.com/JwgtCrIVeEU)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-slideshow-2.jpg)
The script will convert your drawing into a slideshow presentation.
If you select an arrow or line element, the script will use that as the presentation path.
If you select nothing, but the file has a hidden presentation path, the script will use that for determining the slide sequence.
If there are frames, the script will use the frames for the presentation. Frames are played in alphabetical order of their titles.
## Presentation options
- If you select an arrow or line element, the script will use that as the presentation path.
- If you select nothing, but the file has a hidden presentation path, the script will use that for determining the slide sequence.
- If there are frames, the script will use the frames for the presentation. Frames are played in alphabetical order of their titles.
# Keyboard shortcuts and modifier keys
**Forward**: Arrow Down, Arrow Right, or SPACE
**Backward**: Arrow Up, Arrow Left
**Finish presentation**: Backspace, ESC (I had issues with ESC not working in full screen presentation mode on Mac)
**Run presentation in a window**: Hold down the ALT/OPT modifier key when clicking the presentation script button
**Continue presentation**: Hold down SHIFT when clicking the presentation script button. (The feature also works in combination with the ALT/OPT modifier to start the presentation in a window). The feature will only resume while you are within the same Obsidian session (i.e. if you restart Obsidian, slideshow will no longer remember where you were). I have two use cases in mind for this feature:
1) When you are designing your presentation you may want to test how a slide looks. Using this feature you can get back to where you left off by starting the presentation with SHIFT.
2) During presentation you may want to exit presentation mode to show something additional to your audience. You stop the presentation, show the additional thing you wanted, now you want to continue from where you left off. Hold down SHIFT when clicking the slideshow button.
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.17")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const hostLeaf = ea.targetView.leaf;
const hostView = hostLeaf.view;
const statusBarElement = document.querySelector("div.status-bar");
const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierKeyDown.metaKey;
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
const shiftKey = ea.targetView.modifierKeyDown.shiftKey;
const shouldStartWithLastSlide = shiftKey && window.ExcalidrawSlideshow &&
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide === "number")
//-------------------------------
//constants
//-------------------------------
@@ -26,7 +41,7 @@ const TRANSITION_STEP_COUNT = 100;
const TRANSITION_DELAY = 1000; //maximum time for transition between slides in milliseconds
const FRAME_SLEEP = 1; //milliseconds
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
const FADE_LEVEL = 0.15; //opacity of the slideshow controls after fade delay (value between 0 and 1)
const FADE_LEVEL = 0.1; //opacity of the slideshow controls after fade delay (value between 0 and 1)
//using outerHTML because the SVG object returned by Obsidin is in the main workspace window
//but excalidraw might be open in a popout window which has a different document object
const SVG_COG = ea.obsidian.getIcon("lucide-settings").outerHTML;
@@ -36,14 +51,18 @@ const SVG_LEFT_ARROW = ea.obsidian.getIcon("lucide-arrow-left").outerHTML;
const SVG_EDIT = ea.obsidian.getIcon("lucide-pencil").outerHTML;
const SVG_MAXIMIZE = ea.obsidian.getIcon("lucide-maximize").outerHTML;
const SVG_MINIMIZE = ea.obsidian.getIcon("lucide-minimize").outerHTML;
const SVG_LASER_ON = ea.obsidian.getIcon("lucide-hand").outerHTML;
const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
//-------------------------------
//utility & convenience functions
//-------------------------------
let slide = 0;
let isLaserOn = false;
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide : 0;
let isFullscreen = false;
const ownerDocument = ea.targetView.ownerDocument;
const startFullscreen = !altKey;
//The plugin and Obsidian App run in the window object
//When Excalidraw is open in a popout window, the Excalidraw component will run in the ownerWindow
//and in this case ownerWindow !== window
@@ -171,7 +190,7 @@ let preventFullscreenExit = true;
const gotoFullscreen = async () => {
if(isFullscreen) return;
preventFullscreenExit = true;
if(app.isMobile) {
if(ea.DEVICE.isMobile) {
ea.viewToggleFullScreen();
} else {
await contentEl.webkitRequestFullscreen();
@@ -187,8 +206,8 @@ const gotoFullscreen = async () => {
const exitFullscreen = async () => {
if(!isFullscreen) return;
preventFullscreenExit = true;
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(app.isMobile) ea.viewToggleFullScreen();
if(!ea.DEVICE.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(ea.DEVICE.isMobile) ea.viewToggleFullScreen();
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
await waitForExcalidrawResize();
resetControlPanelElPosition();
@@ -251,7 +270,7 @@ const getNavigationRect = ({ x1, y1, x2, y2 }) => {
const { width, height } = excalidrawAPI.getAppState();
const ratioX = width / Math.abs(x1 - x2);
const ratioY = height / Math.abs(y1 - y2);
let ratio = Math.min(Math.max(ratioX, ratioY), 10);
let ratio = Math.min(Math.max(ratioX, ratioY), 30);
const scaledWidth = Math.abs(x1 - x2) * ratio;
const scaledHeight = Math.abs(y1 - y2) * ratio;
@@ -283,7 +302,7 @@ let busy = false;
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSITION_STEP_COUNT) => {
const startTimer = Date.now();
let watchdog = 0;
while(busy && watchdog++<15) await(100);
while(busy && watchdog++<15) await sleep(100);
if(busy && watchdog >= 15) return;
busy = true;
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:true}});
@@ -310,6 +329,9 @@ const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSIT
}
}
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:false}});
if(isLaserOn) {
excalidrawAPI.setActiveTool({type: "laser"});
}
busy = false;
}
@@ -328,6 +350,9 @@ const navigate = async (dir) => {
}
if(selectSlideDropdown) selectSlideDropdown.value = slide+1;
await scrollToNextRect(nextRect);
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide === "number")) {
window.ExcalidrawSlideshow.slide = slide;
}
}
const navigateToSlide = (slideNumber) => {
@@ -450,6 +475,22 @@ const createPresentationNavigationPanel = () => {
margin: 0px auto;`
}
});
el.createEl("button",{
attr: {
title: "Toggle Laser Pointer and Panning Mode"
}
}, button => {
button.innerHTML = isLaserOn ? SVG_LASER_ON : SVG_LASER_OFF;
button.onclick = () => {
isLaserOn = !isLaserOn;
excalidrawAPI.setActiveTool({
type: isLaserOn ? "laser" : "selection"
})
button.innerHTML = isLaserOn ? SVG_LASER_ON : SVG_LASER_OFF;
}
});
el.createEl("button",{
attr: {
title: "Toggle fullscreen. If you hold ALT/OPT when starting the presentation it will not go fullscreen."
@@ -504,12 +545,15 @@ const createPresentationNavigationPanel = () => {
// keyboard navigation
//--------------------
const keydownListener = (e) => {
if(ea.targetView.leaf !== app.workspace.activeLeaf) return;
if(hostLeaf !== app.workspace.activeLeaf) return;
if(hostLeaf.width === 0 && hostLeaf.height === 0) return;
e.preventDefault();
switch(e.key) {
case "Backspace":
case "Escape":
exitPresentation();
break;
case "Space":
case "ArrowRight":
case "ArrowDown":
navigate("fwd");
@@ -605,7 +649,7 @@ const initializeEventListners = () => {
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
controlPanelEl.parentElement?.removeChild(controlPanelEl);
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
}
@@ -620,7 +664,7 @@ const initializeEventListners = () => {
return true;
};
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.addEventListener('fullscreenchange', fullscreenListener);
}
@@ -630,6 +674,9 @@ const initializeEventListners = () => {
// Exit presentation
//----------------------------
const exitPresentation = async (openForEdit = false) => {
//this is a hack, not sure why ea loses target view when other scripts are executed while the presentation is running
ea.targetView = hostView;
isLaserOn = false;
statusBarElement.style.display = "inherit";
if(openForEdit) ea.targetView.preventAutozoom();
await exitFullscreen();
@@ -680,7 +727,8 @@ const exitPresentation = async (openForEdit = false) => {
//Resets pointer offsets. Ugly solution.
//During testing offsets were wrong after presentation, but don't know why.
//This should solve it even if they are wrong.
ea.targetView.refresh();
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
}
@@ -690,6 +738,15 @@ const exitPresentation = async (openForEdit = false) => {
const start = async () => {
statusBarElement.style.display = "none";
ea.setViewModeEnabled(true);
const helpButton = ea.targetView.excalidrawContainer?.querySelector(".ToolIcon__icon.help-icon");
if(helpButton) {
helpButton.style.display = "none";
}
const zoomButton = ea.targetView.excalidrawContainer?.querySelector(".Stack.Stack_vertical.zoom-actions");
if(zoomButton) {
zoomButton.style.display = "none";
}
createPresentationNavigationPanel();
initializeEventListners();
if(startFullscreen) {
@@ -714,7 +771,8 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
}
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
timestamp
timestamp,
slide: 0
};
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}

208
ea-scripts/Split Ellipse.md Normal file
View File

@@ -0,0 +1,208 @@
/*
This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.
There is also the option to close the object along the cut, which will close the cut in the shape of the line.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.jpg)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo2.jpg)
Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
const elements = ea.getViewSelectedElements();
const ellipse = elements.filter(el => el.type == "ellipse")[0];
if (!ellipse) return;
let lines = elements.filter(el => el.type == "line" || el.type == "arrow");
if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow");
const subLines = getSubLines(lines);
const angles = subLines.flatMap(line => {
return intersectionAngleOfEllipseAndLine(ellipse, line.a, line.b).map(result => ({
angle: result,
cuttingLine: line
}));
});
if (angles.length === 0) angles.push({ angle: 0, cuttingLine: null });
angles.sort((a, b) => a.angle - b.angle);
const closeObject = await utils.suggester(["Yes", "No"], [true, false], "Close object along cutedge?")
ea.style.strokeSharpness = closeObject ? "sharp" : "round";
ea.style.strokeColor = ellipse.strokeColor;
ea.style.strokeWidth = ellipse.strokeWidth;
ea.style.backgroundColor = ellipse.backgroundColor;
ea.style.fillStyle = ellipse.fillStyle;
ea.style.roughness = ellipse.roughness;
angles.forEach((angle, key) => {
const cuttingLine = angle.cuttingLine;
angle = angle.angle;
const nextAngleKey = (key + 1) < angles.length ? key + 1 : 0;
const nextAngle = angles[nextAngleKey].angle;
const AngleDelta = nextAngle - angle ? nextAngle - angle : Math.PI*2;
const pointAmount = Math.ceil((AngleDelta*64)/(Math.PI*2));
const stepSize = AngleDelta/pointAmount;
let points = drawEllipse(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.angle, angle, nextAngle, stepSize);
if (closeObject && cuttingLine) points = points.concat(getCutLine(points[0], angles[key], angles[nextAngleKey], ellipse));
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
line.frameId = ellipse.frameId;
line.groupIds = ellipse.groupIds;
});
ea.deleteViewElements([ellipse]);
ea.addElementsToView(false,false,true);
return;
function getSubLines(lines) {
return lines.flatMap((line, key) => {
return line.points.slice(1).map((pointB, i) => ({
a: addVectors([line.points[i], [line.x, line.y]]),
b: addVectors([pointB, [line.x, line.y]]),
originLineIndex: key,
indexPointA: i,
}));
});
}
function intersectionAngleOfEllipseAndLine(ellipse, pointA, pointB) {
/*
To understand the code in this function and subfunctions it might help to take a look at this geogebra file
https://www.geogebra.org/m/apbm3hs6
*/
const c = multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2));
const a = rotateVector(
addVectors([
pointA,
invVec([ellipse.x, ellipse.y]),
invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2)))
]),
-ellipse.angle
)
const l_b = rotateVector(
addVectors([
pointB,
invVec([ellipse.x, ellipse.y]),
invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2)))
]),
-ellipse.angle
);
const b = addVectors([
l_b,
invVec(a)
]);
const solutions = calculateLineSegment(a[0], a[1], b[0], b[1], c[0], c[1]);
return solutions
.filter(num => isBetween(num, 0, 1))
.map(num => {
const point = [
(a[0] + b[0] * num) / ellipse.width,
(a[1] + b[1] * num) / ellipse.height
];
return angleBetweenVectors([1, 0], point);
});
}
function drawEllipse(x, y, width, height, angle = 0, start = 0, end = Math.PI*2, step = Math.PI/32) {
const ellipse = (t) => {
const spanningVector = rotateVector([width/2*Math.cos(t), height/2*Math.sin(t)], angle);
const baseVector = [x+width/2, y+height/2];
return addVectors([baseVector, spanningVector]);
}
if(end <= start) end = end + Math.PI*2;
let points = [];
const almostEnd = end - step/2;
for (let t = start; t < almostEnd; t = t + step) {
points.push(ellipse(t));
}
points.push(ellipse(end))
return points;
}
function getCutLine(startpoint, currentAngle, nextAngle, ellipse) {
if (currentAngle.cuttingLine.originLineIndex != nextAngle.cuttingLine.originLineIndex) return [];
const originLineIndex = currentAngle.cuttingLine.originLineIndex;
if (lines[originLineIndex] == 2) return startpoint;
const originLine = [];
lines[originLineIndex].points.forEach(p => originLine.push(addVectors([
p,
[lines[originLineIndex].x, lines[originLineIndex].y]
])));
const edgepoints = [];
const direction = isInEllipse(originLine[clamp(nextAngle.cuttingLine.indexPointA - 1, 0, originLine.length - 1)], ellipse) ? -1 : 1
let i = isInEllipse(originLine[nextAngle.cuttingLine.indexPointA], ellipse) ? nextAngle.cuttingLine.indexPointA : nextAngle.cuttingLine.indexPointA + direction;
while (isInEllipse(originLine[i], ellipse)) {
edgepoints.push(originLine[i]);
i = (i + direction) % originLine.length;
}
edgepoints.push(startpoint);
return edgepoints;
}
function calculateLineSegment(ax, ay, bx, by, cx, cy) {
const sqrt = Math.sqrt((cx ** 2) * (cy ** 2) * (-(ay ** 2) * (bx ** 2) + 2 * ax * ay * bx * by - (ax ** 2) * (by ** 2) + (bx ** 2) * (cy ** 2) + (by ** 2) * (cx ** 2)));
const numerator = -(ay * by * (cx ** 2) + ax * bx * (cy ** 2));
const denominator = ((by ** 2) * (cx ** 2) + (bx ** 2) * (cy ** 2));
const t1 = (numerator + sqrt) / denominator;
const t2 = (numerator - sqrt) / denominator;
return [t1, t2];
}
function isInEllipse(point, ellipse) {
point = addVectors([point, invVec([ellipse.x, ellipse.y]), invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], 1/2))]);
point = [point[0]*2/ellipse.width, point[1]*2/ellipse.height];
const distance = Math.sqrt(point[0]**2 + point[1]**2);
return distance < 1;
}
function angleBetweenVectors(v1, v2) {
let dotProduct = v1[0] * v2[0] + v1[1] * v2[1];
let determinant = v1[0] * v2[1] - v1[1] * v2[0];
let angle = Math.atan2(determinant, dotProduct);
return angle < 0 ? angle + 2 * Math.PI : angle;
}
function rotateVector (vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function addVectors(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
function invVec(vector) {
return [-vector[0], -vector[1]];
}
function multiplyVectorByScalar(vector, scalar) {
return [vector[0] * scalar, vector[1] * scalar];
}
function round(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
function isBetween(num, min, max) {
return (num >= min && num <= max);
}
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -18,10 +18,10 @@ elements.forEach((el)=>{
ea.style.strokeColor = el.strokeColor;
ea.style.fontFamily = el.fontFamily;
ea.style.fontSize = el.fontSize;
const text = el.text.split("\n");
const text = el.rawText.split("\n");
for(i=0;i<text.length;i++) {
ea.addText(el.x,el.y+i*el.height/text.length,text[i]);
ea.addText(el.x,el.y+i*el.height/text.length,text[i].trim());
}
});
ea.addElementsToView(false,false);
ea.addElementsToView(false,false,true);
ea.deleteViewElements(elements);

View File

@@ -46,4 +46,4 @@ for(i=0;i<el.text.length;i++) {
objectIDs.push(ea.addText(x,y,character));
}
ea.addToGroup(objectIDs);
ea.addElementsToView(true);
ea.addElementsToView(true, false, true);

70
ea-scripts/Text Aura.md Normal file
View File

@@ -0,0 +1,70 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg)
Select a single text element, or a text element in a container. The container must have a transparent background.
The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.
If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.
```js*/
els = ea.getViewSelectedElements();
const isText = (els.length === 1) && els[0].type === "text";
const isContainer = (els.length === 2) &&
((els[0].type === "text" && els[1].id === els[0].containerId && els[1].backgroundColor.toLowerCase() === "transparent") ||
(els[1].type === "text" && els[0].id === els[1].containerId && els[0].backgroundColor.toLowerCase() === "transparent"));
if (!(isText || isContainer)) {
new Notice ("Select a single text element, or a container with a text element and with transparent background color",10000);
return;
}
let strokeColor = ea
.getCM(els.filter(el=>el.type === "text")[0].strokeColor)
.invert({alpha: false})
.stringHEX({alpha: false});
clipboardText = await navigator.clipboard.readText();
if(clipboardText) {
const cm1 = ea.getCM(clipboardText);
if(cm1.format !== "invalid") {
strokeColor = cm1.stringHEX();
} else {
const cm2 = ea.getCM("#"+clipboardText);
if(cm2.format !== "invalid") {
strokeColor = cm2.stringHEX();
}
}
}
const offset = els.filter(el=>el.type === "text")[0].fontSize/24;
let ids = [];
const addClone = (offsetX, offsetY) => {
els.forEach(el=>{
const clone = ea.cloneElement(el);
ids.push(clone.id);
clone.x += offsetX;
clone.y += offsetY;
if(offsetX!==0 || offsetY!==0) {
switch (clone.type) {
case "text":
clone.strokeColor = strokeColor;
break;
default:
clone.strokeColor = "transparent";
break;
}
}
ea.elementsDict[clone.id] = clone;
})
}
addClone(-offset,0);
addClone(offset,0);
addClone(0,offset);
addClone(0,-offset);
addClone(0,0);
ea.copyViewElementsToEAforEditing(els);
els.forEach(el=>ea.elementsDict[el.id].isDeleted = true);
ea.addToGroup(ids);
ea.addElementsToView(false, true, true);

17
ea-scripts/Text Aura.svg Normal file
View File

@@ -0,0 +1,17 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(0 0) rotate(0 60 60)" fill-rule="evenodd"><path d="M0 0 L120 0 L120 40 L80 40 L80 120 L40 120 L40 40 L0 40 L0 0" stroke="none" stroke-width="0" fill="red" fill-rule="evenodd"></path><path d="M0 0 C41.51 0, 83.02 0, 120 0 M0 0 C30.58 0, 61.16 0, 120 0 M120 0 C120 12.11, 120 24.22, 120 40 M120 0 C120 13.92, 120 27.84, 120 40 M120 40 C108.75 40, 97.49 40, 80 40 M120 40 C107.65 40, 95.29 40, 80 40 M80 40 C80 66.51, 80 93.01, 80 120 M80 40 C80 70.33, 80 100.66, 80 120 M80 120 C66.08 120, 52.16 120, 40 120 M80 120 C70.07 120, 60.13 120, 40 120 M40 120 C40 89.21, 40 58.42, 40 40 M40 120 C40 92.66, 40 65.33, 40 40 M40 40 C25.35 40, 10.71 40, 0 40 M40 40 C27.7 40, 15.4 40, 0 40 M0 40 C0 24.03, 0 8.05, 0 0 M0 40 C0 27.82, 0 15.65, 0 0 M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0" stroke="transparent" stroke-width="0.5" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(110 10) rotate(0 -50 50)" fill-rule="evenodd"><path d="M0 0 L-100 0 L-100 20 L-60 20 L-60 100 L-40 100 L-40 20 L0 20 L0 0" stroke="none" stroke-width="0" fill="currentColor" fill-rule="evenodd"></path><path d="M0 0 C-23.27 0, -46.54 0, -100 0 M0 0 C-23.31 0, -46.62 0, -100 0 M-100 0 C-100 6.13, -100 12.26, -100 20 M-100 0 C-100 5.84, -100 11.69, -100 20 M-100 20 C-87.37 20, -74.74 20, -60 20 M-100 20 C-88.34 20, -76.68 20, -60 20 M-60 20 C-60 37.78, -60 55.56, -60 100 M-60 20 C-60 39.34, -60 58.68, -60 100 M-60 100 C-52.58 100, -45.17 100, -40 100 M-60 100 C-54.72 100, -49.43 100, -40 100 M-40 100 C-40 83.83, -40 67.67, -40 20 M-40 100 C-40 77.76, -40 55.51, -40 20 M-40 20 C-25.4 20, -10.8 20, 0 20 M-40 20 C-28.47 20, -16.93 20, 0 20 M0 20 C0 15.42, 0 10.84, 0 0 M0 20 C0 14.54, 0 9.08, 0 0 M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0" stroke="currentColor" stroke-width="0.5" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

33
ea-scripts/Toggle Grid.md Normal file
View File

@@ -0,0 +1,33 @@
/*
Toggles the grid on and off. Especially useful when drawing with just a pen without a mouse or keyboard, as toggling the grid by left-clicking with the pen is sometimes quite tedious.
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.11")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const api = ea.getExcalidrawAPI();
let {gridSize, previousGridSize} = api.getAppState();
if (!previousGridSize) {
previousGridSize = 20
}
if (!gridSize) {
gridSize = previousGridSize;
}
else
{
previousGridSize = gridSize;
gridSize = null;
}
ea.viewUpdateScene({
appState:{
gridSize,
previousGridSize
},
commitToHistory:false
});

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 489">
<path
d="M 20.803582,0.35478208 A 25,25 0 0 0 5.9442069,8.8176728 25,25 0 0 0 8.8172543,44.055954 L 31.254754,63.108689 c -0.121266,0.849954 -0.301322,1.680716 -0.388672,2.541015 -0.218469,2.151668 -0.330078,4.335551 -0.330078,6.544922 V 392.19462 c 0,2.20625 0.111609,4.38587 0.330078,6.53516 0.218468,2.14929 0.544482,4.26807 0.970703,6.34961 0.426219,2.08154 0.952918,4.12591 1.576172,6.12891 0.623252,2.00299 1.342775,3.96328 2.152343,5.87695 0.80957,1.91367 1.708193,3.7802 2.69336,5.59375 0.985166,1.81355 2.056984,3.57472 3.207031,5.27734 1.150047,1.70264 2.379385,3.34681 3.683594,4.92774 1.304208,1.58093 2.683206,3.09844 4.130859,4.54687 1.447654,1.44844 2.964542,2.82767 4.544922,4.13282 1.58038,1.30514 3.223392,2.53642 4.925781,3.6875 1.70239,1.15106 3.463664,2.22277 5.277344,3.20898 1.81368,0.98621 3.679496,1.88672 5.59375,2.69727 1.914254,0.81053 3.87675,1.5302 5.880859,2.15429 2.00411,0.6241 4.049565,1.15323 6.132813,1.58008 2.083248,0.42686 4.203801,0.75188 6.355469,0.9707 2.151667,0.21883 4.335552,0.33204 6.544922,0.33203 H 478.53601 c 2.20625,0 4.38587,-0.1132 6.53515,-0.33203 2.14929,-0.21882 4.26808,-0.54384 6.34961,-0.9707 0.30707,-0.063 0.59887,-0.16503 0.9043,-0.23242 l 33.48047,28.43164 a 25,25 0 0 0 35.23828,-2.87305 25,25 0 0 0 -2.87305,-35.23828 L 41.182488,5.9446259 A 25,25 0 0 0 29.485222,0.40556338 25,25 0 0 0 20.803582,0.35478208 Z M 94.536004,8.1946259 c -2.209366,0 -4.39326,0.1116097 -6.544922,0.3300781 -2.151664,0.2184684 -4.272226,0.5425319 -6.355469,0.9687499 -2.083244,0.42622 -4.128707,0.9548741 -6.132813,1.5781251 -2.004105,0.623253 -3.966609,1.340824 -5.880859,2.150391 -0.337447,0.142712 -0.651869,0.326303 -0.986328,0.474609 l 68.884767,58.498047 h 93.49024 23.52539 v 19.978516 79.392578 l 109.07422,92.6289 h 93.49218 21.4336 v 18.20313 79.39258 l 60.42383,51.31445 c 0.22119,-0.63745 0.49391,-1.25011 0.69531,-1.89648 0.62409,-2.00299 1.15127,-4.04738 1.57812,-6.12891 0.42686,-2.08153 0.75188,-4.20033 0.97071,-6.34961 0.21882,-2.14928 0.33203,-4.32892 0.33203,-6.53516 V 336.74736 271.15166 72.194626 c 0,-2.209349 -0.11321,-4.393275 -0.33203,-6.544922 -0.21882,-2.151647 -0.54386,-4.272242 -0.97071,-6.355469 -0.42685,-2.083227 -0.95403,-4.128722 -1.57812,-6.132812 -0.62409,-2.00409 -1.34376,-3.966624 -2.1543,-5.88086 -0.81054,-1.914234 -1.71302,-3.780086 -2.69922,-5.59375 -0.98619,-1.813662 -2.05792,-3.574971 -3.20898,-5.277343 -1.15106,-1.702373 -2.38041,-3.34737 -3.68555,-4.927735 -1.30514,-1.580364 -2.68439,-3.097282 -4.13281,-4.544922 -1.44842,-1.447638 -2.96596,-2.82471 -4.54688,-4.128906 -1.5809,-1.304195 -3.22708,-2.535511 -4.92968,-3.685547 -1.70262,-1.150034 -3.46187,-2.219921 -5.27539,-3.205078 -1.81353,-0.985156 -3.68011,-1.885752 -5.59375,-2.695312 -1.91366,-0.809561 -3.87593,-1.527143 -5.87891,-2.150391 -2.00298,-0.623246 -4.04739,-1.1519091 -6.12891,-1.5781251 -2.08151,-0.426215 -4.20034,-0.7502832 -6.34961,-0.9687499 -2.14925,-0.2184666 -4.32893,-0.3300781 -6.53515,-0.3300781 H 232.88952 155.64538 Z M 318.53601,72.194626 h 160 V 200.19462 H 458.97937 381.73718 318.53601 V 146.52275 80.927048 Z M 94.536004,116.84892 192.67859,200.19462 H 94.536004 Z m 0,147.3457 H 254.53601 v 128 H 94.536004 Z m 224.000006,42.87891 100.23437,85.12109 H 318.53601 Z" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -15,39 +15,26 @@ In the `Command Palette` installed scripts are prefixed with `Downloaded/`, thus
## Attention developers and hobby hackers
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/hobby-programmer.svg' align='left' style='background-color:whitesmoke; width:80px; margin-right:15px; margin-bottom:10px;'/>
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is availble (hope to build this feature in the future).
If you want to modify scripts, I recommend moving them to the `Excalidraw Automate script folder` or a different subfolder under the script folder. Scripts in the `Downloaded` folder will be overwritten when you click the `Update this script` button. Note also, that at this time, I do not check if the script file has been updated on GitHub, thus the `Update this script` button is always visible once you have installed a script, not only when an update is available (hope to build this feature in the future).
I would love to include your contribution in the script library. If you have a script of your own that you would like to share with the community, please open a [PR](https://github.com/zsviczian/obsidian-excalidraw-plugin/pulls) on GitHub. Be sure to include the following in your pull request
- The [script file](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) with a self explanetory name. The name of the file will be the name of the script in the Command Palette.
- An [image](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/images) explaining the scripts purpose. Remember a picture speaks thousand words!
- An update to this file [ea-scripts/index.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index.md)
- An update to this file [ea-scripts/index-new.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index-new.md)
---
# List of available scripts
## Layout and Organization
**Keywords**: Design, Placement, Arrangement, Structure, Formatting, Alignment
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.svg"/></div>|[[#Auto Layout]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.svg"/></div>|[[#Box Each Selected Groups]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Selected%20Elements.svg"/></div>|[[#Box Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.svg"/></div>|[[#Create DrawIO file]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Ellipse%20Selected%20Elements.svg"/></div>|[[#Ellipse Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles horizontally keep text centered]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally.svg"/></div>|[[#Expand rectangles horizontally]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20vertically%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles vertically keep text centered]]|
@@ -57,39 +44,120 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20spacing.svg"/></div>|[[#Fixed spacing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance%20between%20centers.svg"/></div>|[[#Fixed vertical distance between centers]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance.svg"/></div>|[[#Fixed vertical distance]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.svg"/></div>|[[#Folder Note Core - Make Current Drawing a Folder]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
## Connectors and Arrows
**Keywords**: Links, Relations, Paths, Direction, Flow, Connections
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.svg"></div>|[[#Concatenate lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
## Text Manipulation
**Keywords**: Editing, Font Control, Wording, Typography, Annotation, Modification
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.svg"/></div>|[[#Relative Font Size Cycle]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
## Styling and Appearance
**Keywords**: Design, Look, Visuals, Graphics, Aesthetics, Presentation
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Invert%20colors.svg"/></div>|[[#Invert colors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Lighten%20background%20color.svg"/></div>|[[#Lighten background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Modify%20background%20color%20opacity.svg"/></div>|[[#Modify background color opacity]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line%20Legacy.svg"/></div>|[[#Organic Line Legacy]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.svg"/></div>|[[#Toggle Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
## Linking and Embedding
**Keywords**: Attach, Incorporate, Integrate, Associate, Insert, Reference
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.svg"/></div>|[[#Create DrawIO file]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.svg"/></div>|[[#Folder Note Core - Make Current Drawing a Folder]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
## Utilities and Tools
**Keywords**: Functionalities, Instruments, Helpers, Aids, Features, Enhancements
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.svg"/></div>|[[#Custom Zoom]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.svg"/></div>|[[#Excalidraw Writing Machine]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.svg"/></div>|[[#Repeat Texts]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.svg"/></div>|[[#Split Ellipse]]|
## Collaboration and Export
**Keywords**: Sharing, Teamwork, Exporting, Distribution, Cooperative, Publish
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
## Conversation and Creation
**Keywords**: Transform, Generate, Craft, Produce, Change, Originate
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
## Masking and cropping
**Keywords**: Crop, Mask, Transform images
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Crop%20Vintage%20Mask.svg"/></div>|[[#Crop Vintage Mask]]|
---
# Description and Installation
## Add Connector Point
```excalidraw-script-install
@@ -127,6 +195,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Auto%20Layout.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script performs automatic layout for the selected top-level grouping objects. It is powered by <a href='https://github.com/kieler/elkjs'>elkjs</a> and needs to be connected to the Internet.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-auto-layout.png'></td></tr></table>
## Boolean Operations
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Boolean%20Operations.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">With This Script it is possible to make boolean Operations on Shapes.<br>The style of the resulting shape will be the style of the highest ranking Element that was used.<br>The ranking of the elements is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.<br>The ranking is also important for the difference operation, so a transparent object for example will cut a hole into a solid object.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png'></td></tr></table>
## Box Each Selected Groups
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.md
@@ -145,6 +220,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Change%20shape%20of%20selected%20elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script allows you to change the shape and fill style of selected Rectangles, Diamonds, Ellipses, Lines, Arrows and Freedraw.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-change-shape.jpg'></td></tr></table>
## Concatenate lines
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Concatenate%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-concatenate-lines.png'></td></tr></table>
## Connect elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.md
@@ -187,6 +268,18 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will prompt you for a filename, then create a new markdown document with the file name provided, open the new markdown document in an adjacent pane, and embed the markdown document into the active Excalidraw drawing.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-create-and-embed-new-markdown-file.jpg'></td></tr></table>
## Crop Vintage Mask
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Crop%20Vintage%20Mask.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Crop%20Vintage%20Mask.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Adds a rounded mask to the image by adding a full cover black mask and a rounded rectangle white mask. The script is also useful for adding just a black mask. In this case, run the script, then delete the white mask and add your custom white mask.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-crop-vintage.jpg'></td></tr></table>
## Custom Zoom
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Custom%20Zoom.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can set a custom zoom level with this script. This allows you to set a zoom level below 10% or set the zoom level to a specific value. Note however, that Excalidraw has a bug under 10% zoom... a phantom copy of your image may appear on screen. If this happens, increase the zoom and the phantom should disappear, if it doesn't, then close and open the drawing.</td></tr></table>
## Darken background color
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.md
@@ -197,7 +290,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/HRtaaD34Zzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></td></tr></table>
## Elbow connectors
```excalidraw-script-install
@@ -277,12 +370,39 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script adds the `Folder Note Core: Make current document folder note` function to Excalidraw drawings. Running this script will convert the active Excalidraw drawing into a folder note. If you already have embedded images in your drawing, those attachments will not be moved when the folder note is created. You need to take care of those attachments separately, or convert the drawing to a folder note prior to adding the attachments. The script requires the <a href="https://github.com/aidenlx/folder-note-core" target="_blank">Folder Note Core</a> plugin.</td></tr></table>
## Golden Ratio
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Golden%20Ratio.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Golden%20Ratio.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script performs two different functions depending on the elements selected in the view.<br>
1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again.<br>
2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/golden-ratio.jpg'><br><iframe width="400" height="225" src="https://www.youtube.com/embed/2SHn_ruax-s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Grid selected images
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/7flash'>@7flash</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Grid%20Selected%20Images.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script arranges selected images into compact grid view, removing gaps in-between, resizing when necessary and breaking into multiple rows/columns.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid-selected-images.png'></td></tr></table>
## ExcaliAI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Excalidraw Writing Machine
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script was discontinued in favor of ExcaliAI. Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Hardware Eraser Support
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md
@@ -349,6 +469,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><a href='https://youtu.be/Kwt_8WdOUT4' target='_blank'>Link to video on YouTube</a></td></tr></table>
## Relative Font Size Cycle
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Relative%20Font%20Size%20Cycle.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Relative%20Font%20Size%20Cycle.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will cycle through S, M, L, XL font sizes scaled to the current canvas zoom.</td></tr></table>
## Rename Image
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.md
@@ -361,6 +487,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will detect the difference between 2 selected elements, including position, size, angle, stroke and background color, and create several elements that repeat these differences based on the number of repetitions entered by the user.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-repeat-elements.png'></td></tr></table>
## Repeat Texts
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Texts.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/soraliu'>@soraliu</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Repeat%20Texts.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">In the following script, we address the concept of repetition through the lens of numerical progression. As visualized by the image, where multiple circles each labeled with an even task number are being condensed into a linear sequence, our script will similarly iterate through a set of numbers</td></tr></table>
## Reverse arrows
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.md
@@ -383,7 +515,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
## Set background color of unclosed line object by adding a shadow clone
```excalidraw-script-install
@@ -433,6 +565,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will convert your drawing into a slideshow presentation.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Split Ellipse
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Split%20Ellipse.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.<br>There is also the option to close the object along the cut, which will close the cut in the shape of the line.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo2.png'><br>Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.</td></tr></table>
## Split text by lines
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.md
@@ -445,6 +583,18 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
## Text Aura
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
## Toggle Grid
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Toggle%20Grid.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Toggles the grid on and off.<br> Especially useful when drawing with just a pen without a mouse or keyboard, as toggling the grid by left-clicking with the pen is sometimes quite tedious.</table>
## Text to Sticky Notes
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.md
@@ -461,4 +611,4 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)</td></tr></table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 861 KiB

BIN
images/golden-ratio.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
images/scripts-repeat-texts.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/vintage-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.9.6.1-beta",
"version": "2.4.0-rc-1",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,11 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.9.14",
"version": "2.3.0",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"authorUrl": "https://www.zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-excalidraw-plugin",
"version": "1.9.14",
"version": "2.2.5",
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -11,57 +11,71 @@
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js -w",
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js",
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix"
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix",
"madge": "madge --circular ."
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@zsviczian/excalidraw": "0.15.2-obsidian-11",
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-43",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
"gl-matrix": "^3.4.3",
"lz-string": "^1.5.0",
"lucide-react": "^0.263.1",
"mathjax-full": "^3.2.2",
"monkey-around": "^2.3.0",
"nanoid": "^4.0.2",
"polybooljs": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"roughjs": "^4.5.2",
"html2canvas": "^1.4.1",
"@popperjs/core": "^2.11.8",
"nanoid": "^4.0.2",
"lucide-react": "^0.263.1"
"js-yaml": "^4.1.0",
"opentype.js": "^1.3.4",
"woff2sfnt-sfnt2woff": "^1.0.0"
},
"devDependencies": {
"dotenv": "^16.4.5",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@excalidraw/eslint-config": "^1.0.3",
"@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.2",
"@rollup/plugin-typescript": "^11.1.6",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/node": "^20.4.8",
"@types/react-dom": "^18.2.7",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/js-yaml": "^4.0.9",
"@types/opentype.js": "^1.3.8",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"cssnano": "^6.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"obsidian": "^1.4.0",
"lz-string": "^1.5.0",
"obsidian": "1.5.7-1",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^4.9.5"
"typescript": "^5.2.2",
"@codemirror/commands": "^6.3.3",
"@codemirror/language": "^6.10.0",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -1,19 +1,20 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { env } from "process";
import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import ttypescript from "ttypescript";
import typescript2 from "rollup-plugin-typescript2";
import webWorker from "rollup-plugin-web-worker-loader";
import fs from'fs';
import fs from 'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
import cssnano from 'cssnano';
const isProd = (process.env.NODE_ENV === "production")
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
@@ -26,67 +27,96 @@ const react_pkg = isLib ? "" : isProd
const reactdom_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
if (!isLib) {
const excalidraw_styles = isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.production.css", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/styles.development.css", "utf8");
const plugin_styles = fs.readFileSync("./styles.css", "utf8");
const styles = plugin_styles + excalidraw_styles;
cssnano()
.process(styles) // Process the CSS
.then(result => {
fs.writeFileSync(`./${DIST_FOLDER}/styles.css`, result.css);
})
.catch(error => {
console.error('Error while processing CSS:', error);
});
}
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
!isLib && console.log(manifest.version);
if (!isLib) console.log(manifest.version);
const packageString = isLib ? "" : ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);' +
'const PLUGIN_VERSION="'+manifest.version+'";';
const packageString = isLib
? ""
: ';' + lzstring_pkg +
'\nlet EXCALIDRAW_PACKAGES = LZString.decompressFromBase64("' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '");\n' +
'let {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${EXCALIDRAW_PACKAGES};' +
'return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
'let PLUGIN_VERSION="' + manifest.version + '";';
const BASE_CONFIG = {
input: 'src/main.ts',
external: ['obsidian', '@zsviczian/excalidraw', 'react', 'react-dom'],
}
external: [
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
'obsidian',
'@zsviczian/excalidraw',
'react',
'react-dom'
],
};
const getRollupPlugins = (tsconfig, ...plugins) =>
[
typescript2(tsconfig),
nodeResolve({ browser: true }),
commonjs(),
webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }),
].concat(plugins);
const getRollupPlugins = (tsconfig, ...plugins) => [
typescript2(tsconfig),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
].concat(plugins);
const BUILD_CONFIG = {
...BASE_CONFIG,
output: {
dir: '.',
sourcemap: isProd?false:'inline',
dir: DIST_FOLDER,
entryFileNames: 'main.js',
format: 'cjs',
exports: 'default',
},
plugins: [
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
}),
babel({
exclude: "node_modules/**"
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
typescript({inlineSources: !isProd}),
...isProd
? [
terser({toplevel: false, compress: {passes: 2}}),
plugins: getRollupPlugins(
{tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"},
...(isProd ? [
terser({ toplevel: false, compress: { passes: 2 } }),
//!postprocess - the version available on npmjs does not work, need this update:
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
// https://github.com/developit/rollup-plugin-postprocess/issues/10
postprocess([
[/,React=require\("react"\);/, packageString],
])
]
: [
postprocess([
[/var React = require\('react'\);/, packageString],
])
],
],
}
[/React=require\("react"\),state=require\("@codemirror\/state"\),view=require\("@codemirror\/view"\)/,
`state=require("@codemirror/state"),view=require("@codemirror/view")` + packageString],
]),
] : [
postprocess([ [/var React = require\('react'\);/, packageString] ]),
]),
copy({
targets: [ { src: 'manifest.json', dest: DIST_FOLDER } ],
verbose: true,
}),
),
};
const LIB_CONFIG = {
...BASE_CONFIG,
@@ -98,16 +128,16 @@ const LIB_CONFIG = {
name: "Excalidraw (Library)",
},
plugins: getRollupPlugins(
{ tsconfig: "tsconfig-lib.json", typescript: ttypescript },
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
}
{ tsconfig: "tsconfig-lib.json" },
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
};
let config = [];
if(process.env.NODE_ENV === "lib") {
if (process.env.NODE_ENV === "lib") {
config.push(LIB_CONFIG);
} else {
config.push(BUILD_CONFIG);
}
export default config;
export default config;

View File

@@ -0,0 +1,49 @@
import { Extension } from "@codemirror/state";
import ExcalidrawPlugin from "src/main";
import { HideTextBetweenCommentsExtension } from "./Fadeout";
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
const editorExtensions: {[key:string]:Extension}= {
[EDITOR_FADEOUT]: HideTextBetweenCommentsExtension,
}
export class EditorHandler {
private activeEditorExtensions: Extension[] = [];
constructor(private plugin: ExcalidrawPlugin) {}
destroy(): void {
this.plugin = null;
}
setup(): void {
this.plugin.registerEditorExtension(this.activeEditorExtensions);
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
}
updateCMExtensionState(
extensionIdentifier: string,
extensionState: boolean,
) {
const extension = editorExtensions[extensionIdentifier];
if(!extension) return;
if (extensionState == true) {
this.activeEditorExtensions.push(extension);
// @ts-ignore
this.activeEditorExtensions[this.activeEditorExtensions.length - 1].exID = extensionIdentifier;
} else {
for (let i = 0; i < this.activeEditorExtensions.length; i++) {
const ext = this.activeEditorExtensions[i];
// @ts-ignore
if (ext.exID === extensionIdentifier) {
this.activeEditorExtensions.splice(i, 1);
break;
}
}
}
this.plugin.app.workspace.updateOptions();
}
update(): void {
this.plugin.app.workspace.updateOptions();
}
}

View File

@@ -0,0 +1,66 @@
import { RangeSetBuilder } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
const o30 = Decoration.line({ attributes: {class: "ex-opacity-30"} });
const o15 = Decoration.line({ attributes: {class: "ex-opacity-15"} });
const o8 = Decoration.line({ attributes: {class: "ex-opacity-8"} });
const o5 = Decoration.line({ attributes: {class: "ex-opacity-5"} });
const o0 = Decoration.line({ attributes: {class: "ex-opacity-0"} });
export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
class {
view: EditorView;
decorations: DecorationSet;
reExcalidrawData = /^%%(?:\r\n|\r|\n)# Excalidraw Data$/gm;
reTextElements = /^%%(?:\r\n|\r|\n)# Text Elements$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)##? Drawing$/gm;
linecount = 0;
isExcalidraw = false;
constructor(view: EditorView) {
this.view = view;
this.isExcalidraw = view.state.doc.toString().search(/^excalidraw-plugin: /m) > 0;
if(!this.isExcalidraw) {
this.decorations = Decoration.none;
return;
}
this.decorations = this.updateDecorations(view);
}
updateDecorations (view: EditorView) {
const { state } = view;
const { doc } = state;
const text = doc.toString();
let start = text.search(this.reExcalidrawData);
if(start == -1) {
start = text.search(this.reTextElements);
}
if(start == -1) {
start = text.search(this.reDrawing);
}
if(start == -1) return Decoration.none;
const startLine = doc.lineAt(start).number;
const endLine = doc.lines;
let builder = new RangeSetBuilder<Decoration>()
for (let l = startLine; l <= endLine; l++) {
const line = doc.line(l);
const pos = l-startLine;
builder.add(line.from, line.from,
pos == 0 ? o30 : (pos == 1) ? o15 : (pos < 6) ? o8 : (pos < 12) ? o5 : o0);
}
return builder.finish()
}
update(update: ViewUpdate) {
if (this.isExcalidraw && update.docChanged) {
this.decorations = this.updateDecorations(update.view)
}
}
},
{
decorations: (x) => x.decorations,
}
);

View File

@@ -1,29 +1,25 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
CASCADIA_FONT,
import {
DEFAULT_MD_EMBED_CSS,
fileid,
FRONTMATTER_KEY_BORDERCOLOR,
FRONTMATTER_KEY_FONT,
FRONTMATTER_KEY_FONTCOLOR,
FRONTMATTER_KEY_MD_STYLE,
IMAGE_TYPES,
nanoid,
THEME_FILTER,
VIRGIL_FONT,
} from "./Constants";
FRONTMATTER_KEYS,
getCSSFontDefinition,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension } from "./utils/FileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -37,9 +33,15 @@ import {
hasExportTheme,
LinkParts,
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
cropCanvas,
} from "./utils/Utils";
import { ValueOf } from "./types";
import { has } from "./svgToExcalidraw/attributes";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -54,6 +56,7 @@ export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
@@ -102,10 +105,14 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
if(typeof svg === 'string') {
// Replace colors in the SVG string
for (const [oldColor, newColor] of Object.entries(colorMap)) {
const fillRegex = new RegExp(`fill="${oldColor}"`, 'g');
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'g');
const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi');
svg = svg.replaceAll(fillStyleRegex, `fill:${newColor}`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi');
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
const strokeStyleRegex = new RegExp(`stroke:${oldColor}`, 'gi');
svg = svg.replaceAll(strokeStyleRegex, `stroke:${newColor}`);
}
return svg;
}
@@ -113,8 +120,8 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
// Modify the fill and stroke attributes of child nodes
const childNodes = (node: ChildNode) => {
if (node instanceof SVGElement) {
const oldFill = node.getAttribute('fill');
const oldStroke = node.getAttribute('stroke');
const oldFill = node.getAttribute('fill')?.toLocaleLowerCase();
const oldStroke = node.getAttribute('stroke')?.toLocaleLowerCase();
if (oldFill && colorMap[oldFill]) {
node.setAttribute('fill', colorMap[oldFill]);
@@ -135,8 +142,6 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
return svg;
}
export class EmbeddedFile {
public file: TFile = null;
public isSVGwithBitmap: boolean = false;
@@ -147,18 +152,20 @@ export class EmbeddedFile {
public mimeType: MimeType = "application/octet-stream";
public size: Size = { height: 0, width: 0 };
public linkParts: LinkParts;
public filenameparts: FILENAMEPARTS
private hostPath: string;
public attemptCounter: number = 0;
public isHyperlink: boolean = false;
public isHyperLink: boolean = false;
public isLocalLink: boolean = false;
public hyperlink:DataURL;
public colorMap: ColorMap | null = null;
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON) : null;
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
this.colorMap = null;
}
@@ -169,12 +176,18 @@ export class EmbeddedFile {
this.imgInverted = this.img = "";
this.mtime = 0;
if(imgPath.startsWith("https://") || imgPath.startsWith("http://")){
this.isHyperlink = true;
if(imgPath.startsWith("https://") || imgPath.startsWith("http://") || imgPath.startsWith("ftp://") || imgPath.startsWith("ftps://")) {
this.isHyperLink = true;
this.hyperlink = imgPath as DataURL;
return;
};
if(imgPath.startsWith("file://")) {
this.isLocalLink = true;
this.hyperlink = imgPath as DataURL;
return;
}
this.linkParts = getLinkParts(imgPath);
this.hostPath = hostPath;
if (!this.linkParts.path) {
@@ -187,7 +200,7 @@ export class EmbeddedFile {
if (!this.linkParts.height) {
this.linkParts.height = this.plugin.settings.mdSVGmaxHeight;
}
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
hostPath,
);
@@ -198,15 +211,18 @@ export class EmbeddedFile {
5000,
);
}
} else {
this.filenameparts = getEmbeddedFilenameParts(imgPath);
this.filenameparts.filepath = this.file.path;
}
}
private fileChanged(): boolean {
if(this.isHyperlink) {
if(this.isHyperLink || this.isLocalLink) {
return false;
}
if (!this.file) {
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
this.hostPath,
); // maybe the file has synchronized in the mean time
@@ -215,7 +231,7 @@ export class EmbeddedFile {
return false;
}
}
return this.mtime != this.file.stat.mtime;
return this.mtime !== this.file.stat.mtime;
}
public setImage(
@@ -225,13 +241,13 @@ export class EmbeddedFile {
isDark: boolean,
isSVGwithBitmap: boolean,
) {
if (!this.file && !this.isHyperlink) {
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
return;
}
if (this.fileChanged()) {
this.imgInverted = this.img = "";
}
this.mtime = this.isHyperlink ? 0 : this.file.stat.mtime;
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
this.size = size;
this.mimeType = mimeType;
switch (isDark && isSVGwithBitmap) {
@@ -246,7 +262,7 @@ export class EmbeddedFile {
}
public isLoaded(isDark: boolean): boolean {
if(!this.isHyperlink) {
if(!this.isHyperLink && !this.isLocalLink) {
if (!this.file) {
this.file = app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
@@ -268,7 +284,7 @@ export class EmbeddedFile {
}
public getImage(isDark: boolean) {
if (!this.file && !this.isHyperlink) {
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
return "";
}
if (isDark && this.isSVGwithBitmap) {
@@ -282,7 +298,7 @@ export class EmbeddedFile {
* @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100%
*/
public shouldScale() {
return this.isHyperlink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
return this.isHyperLink || this.isLocalLink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
}
}
@@ -317,12 +333,148 @@ export class EmbeddedFilesLoader {
return result;
}
private async getExcalidrawSVG ({
isDark,
file,
depth,
inFile,
hasSVGwithBitmap,
elements = [],
}: {
isDark: boolean;
file: TFile;
depth: number;
inFile: TFile | EmbeddedFile;
hasSVGwithBitmap: boolean;
elements?: ExcalidrawElement[];
}) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const isMask = isMaskFile(this.plugin, file);
const forceTheme = hasExportTheme(this.plugin, file)
? getExportTheme(this.plugin, file, "light")
: undefined;
const exportSettings: ExportSettings = {
withBackground: hasExportBackground(this.plugin, file)
? getWithBackground(this.plugin, file)
: false,
withTheme: !!forceTheme,
isMask,
skipInliningFonts: false,
};
const hasColorMap = Boolean(inFile instanceof EmbeddedFile ? inFile.colorMap : null);
const shouldUseCache = !hasColorMap && this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady();
const hasFilenameParts = Boolean((inFile instanceof EmbeddedFile) && inFile.filenameparts);
const filenameParts = hasFilenameParts ? (inFile as EmbeddedFile).filenameparts : null;
const cacheKey:ImageKey = {
...hasFilenameParts? {
...filenameParts,
inlineFonts: !exportSettings.skipInliningFonts,
}: {
filepath: file.path,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasClippedFrameref: false,
hasSectionref: false,
inlineFonts: !exportSettings.skipInliningFonts,
blockref: null,
sectionref: null,
linkpartReference: null,
linkpartAlias: null,
},
isDark,
previewImageType: PreviewImageType.SVG,
scale: 1,
isTransparent: !exportSettings.withBackground,
}
const maybeSVG = shouldUseCache
? await imageCache.getImageFromCache(cacheKey)
: undefined;
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: replaceSVGColors(
await createSVG(
hasFilenameParts
? (filenameParts.hasGroupref || filenameParts.hasBlockref ||
filenameParts.hasSectionref || filenameParts.hasFrameref ||
filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path)
: file?.path,
false, //false
hasFilenameParts && filenameParts.hasClippedFrameref
? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
"image:not([href^='data:image/svg'])",
);
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
u.setAttribute("filter", THEME_FILTER);
});
});
}
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
if(shouldUseCache && !Boolean(maybeSVG)) {
//cache SVG should have the width and height parameters and not the embedded font
//see svgWithFont below
imageCache.addImageToCache(cacheKey,"", svg);
}
if(!svg.hasAttribute("width") && svg.hasAttribute("viewBox")){
//2024.06.09
//this addresses backward compatibility issues where the cache does not have the width and height attributes
//this should be removed in the future
const vb = svg.getAttr("viewBox").split(" ");
Boolean(vb[2]) && svg.setAttribute("width", vb[2]);
Boolean(vb[3]) && svg.setAttribute("height", vb[3]);
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
//this is a fix for backward compatibility - I messed up with generating the local link
private getLocalPath(path: string) {
const localPath = path.split("file://")[1]
if(localPath.startsWith("/")) {
return localPath.substring(1);
}
return localPath;
}
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<ImgData> {
if (!this.plugin || !inFile) {
return null;
}
const isHyperlink = inFile instanceof EmbeddedFile ? inFile.isHyperlink : false;
const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false;
const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false;
const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : "";
const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile;
if(file && markdownRendererRecursionWatcthdog.has(file)) {
@@ -331,7 +483,7 @@ export class EmbeddedFilesLoader {
}
const linkParts =
isHyperlink
isHyperLink
? null
: inFile instanceof EmbeddedFile
? inFile.linkParts
@@ -346,11 +498,11 @@ export class EmbeddedFilesLoader {
};
let hasSVGwithBitmap = false;
const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file);
const isPDF = !isHyperlink && file.extension.toLowerCase() === "pdf";
const isExcalidrawFile = !isHyperLink && !isLocalLink && this.plugin.isExcalidrawFile(file);
const isPDF = !isHyperLink && !isLocalLink && file.extension.toLowerCase() === "pdf";
if (
!isHyperlink && !isPDF &&
!isHyperLink && !isPDF && !isLocalLink &&
!(
IMAGE_TYPES.contains(file.extension) ||
isExcalidrawFile ||
@@ -359,63 +511,26 @@ export class EmbeddedFilesLoader {
) {
return null;
}
const ab = isHyperlink || isPDF
const ab = isHyperLink || isPDF
? null
: await app.vault.readBinary(file);
: isLocalLink
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
: await app.vault.readBinary(file);
const getExcalidrawSVG = async (isDark: boolean) => {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const forceTheme = hasExportTheme(this.plugin, file)
? getExportTheme(this.plugin, file, "light")
: undefined;
const exportSettings: ExportSettings = {
withBackground: hasExportBackground(this.plugin, file)
? getWithBackground(this.plugin, file)
: false,
withTheme: !!forceTheme,
};
const svg = replaceSVGColors(
await createSVG(
file.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
[],
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
let dURL: DataURL = null;
if (isExcalidrawFile) {
const res = await this.getExcalidrawSVG({
isDark: this.isDark,
file,
depth,
inFile,
hasSVGwithBitmap,
});
dURL = res.dataURL;
hasSVGwithBitmap = res.hasSVGwithBitmap;
}
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
"image:not([href^='data:image/svg'])",
);
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
u.setAttribute("filter", THEME_FILTER);
});
});
}
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return dURL as DataURL;
};
const excalidrawSVG = isExcalidrawFile
? await getExcalidrawSVG(this.isDark)
: null;
const excalidrawSVG = isExcalidrawFile ? dURL : null;
const [pdfDataURL, pdfSize] = isPDF
? await this.pdfToDataURL(file,linkParts)
@@ -425,7 +540,7 @@ export class EmbeddedFilesLoader {
? "image/png"
: "image/svg+xml";
const extension = isHyperlink
const extension = isHyperLink || isLocalLink
? getURLImageExtension(hyperlink)
: file.extension;
if (!isExcalidrawFile && !isPDF) {
@@ -433,20 +548,20 @@ export class EmbeddedFilesLoader {
}
let dataURL =
isHyperlink
isHyperLink
? (
inFile instanceof EmbeddedFile
? await getDataURLFromURL(inFile.hyperlink, mimeType)
: null
)
: excalidrawSVG ?? pdfDataURL ??
(file.extension === "svg"
(file?.extension === "svg"
? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null)
: file.extension === "md"
: file?.extension === "md"
? null
: await getDataURL(ab, mimeType));
if(!isHyperlink && !dataURL) {
if(!isHyperLink && !dataURL && !isLocalLink) {
markdownRendererRecursionWatcthdog.add(file);
const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts, depth);
markdownRendererRecursionWatcthdog.delete(file);
@@ -458,10 +573,11 @@ export class EmbeddedFilesLoader {
return {
mimeType,
fileId: await generateIdFromFile(
isHyperlink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
),
dataURL,
created: isHyperlink ? 0 : file.stat.mtime,
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
hasSVGwithBitmap,
size,
};
@@ -473,9 +589,11 @@ export class EmbeddedFilesLoader {
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number
depth:number,
isThemeChange:boolean = false,
) {
if(depth > 4) {
if(depth > 7) {
new Notice(t("INFINITE_LOOP_WARNING")+depth.toString(), 6000);
return;
}
@@ -492,7 +610,7 @@ export class EmbeddedFilesLoader {
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
const data = await this._getObsidianImage(embeddedFile, depth);
if (data) {
const fileData = {
const fileData: FileData = {
mimeType: data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
@@ -509,7 +627,8 @@ export class EmbeddedFilesLoader {
}
//files.push(fileData);
}
} else if (embeddedFile.isSVGwithBitmap) {
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
//this will reload the image in light/dark mode when switching themes
const fileData = {
mimeType: embeddedFile.mimeType,
id: entry.value[0],
@@ -534,7 +653,7 @@ export class EmbeddedFilesLoader {
while (!this.terminate && !(equation = equations.next()).done) {
if (!excalidrawData.getEquation(equation.value[0]).isLoaded) {
const latex = equation.value[1].latex;
const data = await tex2dataURL(latex, this.plugin);
const data = await tex2dataURL(latex);
if (data) {
const fileData = {
mimeType: data.mimeType,
@@ -550,6 +669,59 @@ export class EmbeddedFilesLoader {
}
}
if(shouldRenderMermaid()) {
const mermaidElements = getMermaidImageElements(excalidrawData.scene.elements);
for(const element of mermaidElements) {
if(this.terminate) {
continue;
}
const data = getMermaidText(element);
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
if(!result) {
continue;
}
if(result?.files) {
for (const key in result.files) {
const fileData = {
...result.files[key],
id: element.fileId,
created: Date.now(),
hasSVGwithBitmap: false,
shouldScale: true,
size: await getImageSize(result.files[key].dataURL),
};
files.push(fileData);
}
continue;
}
if(result?.elements) {
//handle case that mermaidToExcalidraw has implemented this type of diagram in the mean time
const res = await this.getExcalidrawSVG({
isDark: this.isDark,
file: null,
depth,
inFile: null,
hasSVGwithBitmap: false,
elements: result.elements
});
if(res?.dataURL) {
const size = await getImageSize(res.dataURL);
const fileData:FileData = {
mimeType: "image/svg+xml",
id: element.fileId,
dataURL: res.dataURL,
created: Date.now(),
hasSVGwithBitmap: res.hasSVGwithBitmap,
size,
shouldScale: true,
};
files.push(fileData);
}
continue;
}
}
};
this.emptyPDFDocsMap();
if (this.terminate) {
return;
@@ -575,6 +747,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
// Render the page
const renderPage = async (num:number) => {
@@ -595,6 +769,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};
@@ -639,16 +830,35 @@ export class EmbeddedFilesLoader {
let fontName = plugin.settings.mdFont;
if (
fileCache?.frontmatter &&
Boolean(fileCache.frontmatter[FRONTMATTER_KEY_FONT])
Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["font"].name])
) {
fontName = fileCache.frontmatter[FRONTMATTER_KEY_FONT];
fontName = fileCache.frontmatter[FRONTMATTER_KEYS["font"].name];
}
switch (fontName) {
case "Virgil":
fontDef = VIRGIL_FONT;
fontDef = await getCSSFontDefinition(1);
break;
case "Cascadia":
fontDef = CASCADIA_FONT;
fontDef = await getCSSFontDefinition(3);
break;
case "Assistant":
case "Helvetica":
fontDef = await getCSSFontDefinition(2);
break;
case "Excalifont":
fontDef = await getCSSFontDefinition(5);
break;
case "Nunito":
fontDef = await getCSSFontDefinition(6);
break;
case "Lilita One":
fontDef = await getCSSFontDefinition(7);
break;
case "Comic Shanns":
fontDef = await getCSSFontDefinition(8);
break;
case "Liberation Sans":
fontDef = await getCSSFontDefinition(9);
break;
case "":
fontDef = "";
@@ -667,15 +877,15 @@ export class EmbeddedFilesLoader {
}
const fontColor = fileCache?.frontmatter
? fileCache.frontmatter[FRONTMATTER_KEY_FONTCOLOR] ??
? fileCache.frontmatter[FRONTMATTER_KEYS["font-color"].name] ??
plugin.settings.mdFontColor
: plugin.settings.mdFontColor;
let style = fileCache?.frontmatter
? fileCache.frontmatter[FRONTMATTER_KEY_MD_STYLE] ?? ""
? fileCache.frontmatter[FRONTMATTER_KEYS["md-css"].name] ?? ""
: "";
let frontmatterCSSisAfile = false;
if (style && style != "") {
if (style && style !== "") {
const f = plugin.app.metadataCache.getFirstLinkpathDest(style, file.path);
if (f) {
style = await plugin.app.vault.read(f);
@@ -695,7 +905,7 @@ export class EmbeddedFilesLoader {
}
const borderColor = fileCache?.frontmatter
? fileCache.frontmatter[FRONTMATTER_KEY_BORDERCOLOR] ??
? fileCache.frontmatter[FRONTMATTER_KEYS["border-color"].name] ??
plugin.settings.mdBorderColor
: plugin.settings.mdBorderColor;
@@ -731,12 +941,14 @@ export class EmbeddedFilesLoader {
mdDIV.style.display = "block";
mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial";
await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
//await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin);
await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin);
mdDIV
.querySelectorAll(":scope > *[class^='frontmatter']")
.forEach((el) => mdDIV.removeChild(el));
await replaceBlobWithBase64(mdDIV); //because image cache returns a blob
const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']"))
for(let i=0;i<internalEmbeds.length;i++) {
const el = internalEmbeds[i];
@@ -848,7 +1060,7 @@ const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Pro
return svgToBase64(svg) as DataURL;
};
export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
let id: FileId;
try {
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
@@ -863,4 +1075,53 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
id = fileid() as FileId;
}
return id;
};*/
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
let id: FileId;
try {
// Convert the file ArrayBuffer to a Uint8Array
const fileArray = new Uint8Array(file);
// If a key is provided, concatenate it to the file data
let dataToHash: Uint8Array;
if (key) {
const encoder = new TextEncoder();
const keyArray = encoder.encode(key);
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
dataToHash.set(fileArray);
dataToHash.set(keyArray, fileArray.length);
} else {
dataToHash = fileArray;
}
// Hash the combined data (file and key, if provided)
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
id =
// Convert buffer to byte array
Array.from(new Uint8Array(hashBuffer))
// Convert to hex string
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("") as FileId;
} catch (error) {
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
id = fileid() as FileId;
}
return id;
};
const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise<void> => {
const images = divElement.querySelectorAll<HTMLImageElement>('img[src^="blob:app://obsidian.md"]');
for (let img of images) {
const blobUrl = img.src;
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const base64 = await blobToBase64(blob);
img.src = `data:${blob.type};base64,${base64}`;
} catch (error) {
console.error(`Failed to fetch or convert blob: ${blobUrl}`, error);
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, Theme } from "@zsviczian/excalidraw/types/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
| ({
@@ -26,6 +27,7 @@ declare namespace ExcalidrawLib {
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
width: number,
height: number,
@@ -44,7 +46,9 @@ declare namespace ExcalidrawLib {
appState?: AppState;
files?: any;
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
skipInliningFonts?: boolean;
}): Promise<SVGSVGElement>;
function sceneCoordsToViewportCoords(
@@ -86,18 +90,36 @@ declare namespace ExcalidrawLib {
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox;
function getContainerElement(
element: ExcalidrawTextElement | null,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null;
function refreshTextDimensions(
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
text: string,
): {
text: string,
x: number,
y: number,
width: number,
height: number,
};
function getMaximumGroups(
elements: ExcalidrawElement[],
elementsMap: ElementsMap,
): ExcalidrawElement[][];
function measureText(
text: string,
font: FontString,
lineHeight: number,
): { width: number; height: number; baseline: number };
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
): { width: number; height: number; };
function getLineHeight (fontFamily: FontFamilyValues):number;
function wrapText(text: string, font: FontString, maxWidth: number): string;
function getFontString({
@@ -108,6 +130,13 @@ declare namespace ExcalidrawLib {
fontFamily: FontFamilyValues;
}): FontString;
function getFontFamilyString ({
fontFamily,
}: {
fontFamily: number;
}): string;
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
function exportToBlob(
@@ -125,4 +154,37 @@ declare namespace ExcalidrawLib {
): TElement;
function getEmbedLink (link: string | null | undefined): EmbeddedLink;
}
function mermaidToExcalidraw(
mermaidDefinition: string,
opts: {fontSize: number},
forceSVG?: boolean,
): Promise<{
elements?: ExcalidrawElement[];
files?: any;
error?: string;
} | undefined>;
var getSceneVersion: any;
var Excalidraw: any;
var MainMenu: any;
var WelcomeScreen: any;
var TTDDialogTrigger: any;
var TTDDialog: any;
var DiagramToCodePlugin: (props: {
generate: GenerateDiagramToCode;
}) => any;
function getDataURL(file: Blob | File): Promise<DataURL>;
function destroyObsidianUtils(): void;
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getCSSFontDefinition(fontFamily: number): Promise<string>;
function getTextFromElements (
elements: readonly ExcalidrawElement[],
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,26 @@
import { DataURL } from "@zsviczian/excalidraw/types/types";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { errorlog, getImageSize, log, sleep, svgToBase64 } from "./utils/Utils";
import { fileid } from "./Constants";
import html2canvas from "html2canvas";
import { Notice } from "obsidian";
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
declare let window: any;
import ExcalidrawView from "./ExcalidrawView";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getImageSize, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants/constants";
import { TFile } from "obsidian";
import { MathDocument } from "mathjax-full/js/core/MathDocument";
export const updateEquation = async (
equation: string,
fileId: string,
view: ExcalidrawView,
addFiles: Function,
plugin: ExcalidrawPlugin,
) => {
const data = await tex2dataURL(equation, plugin);
const data = await tex2dataURL(equation);
if (data) {
const files: FileData[] = [];
files.push({
@@ -33,9 +36,27 @@ export const updateEquation = async (
}
};
let adaptor: LiteAdaptor;
let html: MathDocument<any, any, any>;
let preamble: string;
export const clearMathJaxVariables = () => {
adaptor = null;
html = null;
preamble = null;
};
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
const loadPreamble = async () => {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file && file instanceof TFile
? await app.vault.read(file)
: null;
};
export async function tex2dataURL(
tex: string,
plugin: ExcalidrawPlugin,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
@@ -43,102 +64,47 @@ export async function tex2dataURL(
created: number;
size: { height: number; width: number };
}> {
//if network is slow, or not available, or mathjax has not yet fully loaded
let counter = 0;
while (!plugin.mathjax && !plugin.mathjaxLoaderFinished && counter < 10) {
await sleep(100);
counter++;
}
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
if(!plugin.mathjaxLoaderFinished) {
errorlog({where: "text2dataURL", fn: tex2dataURL, message:"mathjaxLoader not ready, using fallback. Try reloading Obsidian or restarting the Excalidraw plugin"});
if(!adaptor) {
await loadPreamble();
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...Boolean(preamble) ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {},
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
}
//it is not clear why this works, but it seems that after loading the plugin sometimes only the third attempt is successful.
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
await sleep(100);
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
await sleep(100);
try {
return await mathjaxSVG(tex, plugin);
} catch (e) {
if (plugin.mathjax) {
new Notice(
"Unknown error loading LaTeX. Using fallback solution. Try closing and reopening this drawing.",
);
} else {
new Notice(
"LaTeX support did not load. Using fallback solution. Try checking your network connection.",
);
}
//fallback
return await mathjaxImage2html(tex);
const node = html.convert(
Boolean(preamble) ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
}
}
export async function mathjaxSVG(
tex: string,
plugin: ExcalidrawPlugin,
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
const eq = plugin.mathjax.tex2svg(tex, { display: true, scale: 4 });
const svg = eq.querySelector("svg");
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(dataURL),
};
} catch (e) {
console.error(e);
}
return null;
}
async function mathjaxImage2html(tex: string): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
const div = document.body.createDiv();
div.style.display = "table"; //this will ensure div fits width of formula exactly
//@ts-ignore
const eq = window.MathJax.tex2chtml(tex, { display: true, scale: 4 }); //scale to ensure good resolution
eq.style.margin = "3px";
eq.style.color = "black";
//ipad support - removing mml as that was causing phantom double-image blur.
const el = eq.querySelector("mjx-assistive-mml");
if (el) {
el.parentElement.removeChild(el);
}
div.appendChild(eq);
window.MathJax.typeset();
const canvas = await html2canvas(div, { backgroundColor: null }); //transparent
document.body.removeChild(div);
return {
mimeType: "image/png",
fileId: fileid() as FileId,
dataURL: canvas.toDataURL() as DataURL,
created: Date.now(),
size: { height: canvas.height, width: canvas.width },
};
}
}

View File

@@ -1,17 +1,18 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
TFile,
Vault,
} from "obsidian";
import { RERENDER_EVENT } from "./Constants";
import { DEVICE, RERENDER_EVENT } from "./constants/constants";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { createPNG, createSVG } from "./ExcalidrawAutomate";
import { ExportSettings } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {getIMGFilename,} from "./utils/FileUtils";
import {
embedFontsInSVG,
getEmbeddedFilenameParts,
getExportTheme,
getQuickImagePreview,
@@ -19,36 +20,53 @@ import {
getWithBackground,
hasExportTheme,
convertSVGStringToElement,
isMaskFile,
} from "./utils/Utils";
import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
interface imgElementAttributes {
file?: TFile;
fname: string; //Excalidraw filename
fwidth: string; //Display width of image
fheight: string; //Display height of image
style: string; //css style to apply to IMG element
style: string[]; //css style to apply to IMG element
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
if (isNaN(width) || width === 0 || width === null) {
if(getDefaultHeight(plugin)!=="") return "";
return "400";
}
return plugin.settings.width;
};
const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
const height = parseInt(plugin.settings.height);
if (isNaN(height) || height === 0 || height === null) {
return "";
}
return plugin.settings.height;
};
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -61,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -72,7 +91,14 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.PNG,
scale,
isTransparent: !exportSettings.withBackground,
inlineFonts: true, //though for PNG this makes no difference, but the key requires it
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -91,11 +117,13 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
const png =
quickPNG ??
(await createPNG(
(filenameParts.hasGroupref || filenameParts.hasFrameref)
(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -118,13 +146,27 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
} else {
style = "width: fit-content;"
}
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
style += `${imgAttributes.fwidth?"min-":"max-"}height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) element.setAttribute("style", style);
element.addClass(imgAttributes.style);
element.addClass("excalidraw-embedded-img");
element.classList.add(...Array.from(imgAttributes.style))
if(!element.hasClass("excalidraw-embedded-img")) {
element.addClass("excalidraw-embedded-img");
}
if(
window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed &&
!element.hasClass("excalidraw-canvas-immersive")
) {
element.addClass("excalidraw-canvas-immersive");
}
}
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -136,7 +178,17 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVGIMG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -155,13 +207,15 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
}
}
let svg = convertSVGStringToElement((
const svg = convertSVGStringToElement((
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
filenameParts?.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -177,7 +231,6 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
return null;
}
svg = embedFontsInSVG(svg, plugin);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
@@ -193,20 +246,31 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
isDark: theme==="dark",
previewImageType: PreviewImageType.SVG,
scale:1,
isTransparent: !exportSettings.withBackground,
inlineFonts: !exportSettings.skipInliningFonts,
};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
const svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
false,
exportSettings,
filenameParts.hasClippedFrameref
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
: exportSettings,
loader,
theme,
null,
@@ -223,10 +287,14 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
return null;
}
//cache SVG should have the width and height parameters and not the embedded font
if(!Boolean(maybeSVG)) {
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
}
svg.removeAttribute("width");
svg.removeAttribute("height");
containerElement.append(svg);
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
return containerElement;
}
@@ -241,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -253,7 +322,7 @@ const getIMG = async (
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-"));
const forceTheme = hasExportTheme(plugin, file)
? getExportTheme(plugin, file, "light")
@@ -262,6 +331,7 @@ const getIMG = async (
const exportSettings: ExportSettings = {
withBackground: getWithBackground(plugin, file),
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
isMask: isMaskFile(plugin, file),
};
const theme =
@@ -287,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -315,7 +386,11 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
}
imgOrDiv.setAttribute("fileSource", attr.fname);
if (attr.fwidth) {
imgOrDiv.setAttribute("w", attr.fwidth);
@@ -326,7 +401,7 @@ const createImgElement = async (
imgOrDiv.setAttribute("draggable","false");
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
let timer:NodeJS.Timeout;
let timer:number;
const clickEvent = (ev:PointerEvent) => {
if(!(ev.target instanceof Element)) {
return;
@@ -341,12 +416,31 @@ const createImgElement = async (
if (src) {
const srcParts = src.match(/([^#]*)(.*)/);
if(!srcParts) return;
plugin.openDrawing(
vault.getAbstractFileByPath(srcParts[1]) as TFile,
linkClickModifierType(ev),
true,
srcParts[2],
);
const f = vault.getAbstractFileByPath(srcParts[1]) as TFile;
const linkModifier = linkClickModifierType(ev);
if (plugin.isExcalidrawFile(f) && isMaskFile(plugin, f)) {
(async () => {
const linkString = `[[${f.path}${srcParts[2]?"#"+srcParts[2]:""}]] ${getExcalidrawFileForwardLinks(plugin.app, f, new Set<string>())}`;
const result = await linkPrompt(linkString, plugin.app);
if(!result) return;
const [file, linkText, subpath] = result;
if(plugin.isExcalidrawFile(file)) {
plugin.openDrawing(file,linkModifier, true, subpath);
return;
}
let paneType: boolean | PaneType = false;
switch(linkModifier) {
case "active-pane": paneType = false; break;
case "new-pane": paneType = "split"; break;
case "popout-window": paneType = "window"; break;
case "new-tab": paneType = "tab"; break;
case "md-properties": paneType = "tab"; break;
}
plugin.app.workspace.openLinkText(linkText,"",paneType,subpath ? {eState: {subpath}} : {});
})()
return;
}
plugin.openDrawing(f,linkModifier,true,srcParts[2]);
} //.ctrlKey||ev.metaKey);
};
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
@@ -360,17 +454,20 @@ const createImgElement = async (
eventElement.addEventListener("pointermove",(ev)=>{
if(!timer) return;
if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) {
clearTimeout(timer);
window.clearTimeout(timer);
timer = null;
}
});
eventElement.addEventListener("pointerdown",(ev)=>{
if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return;
timer = setTimeout(()=>clickEvent(ev),500);
//@ts-ignore
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin;
const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile;
timer = window.setTimeout(()=>clickEvent(ev),timeoutValue);
pointerDownEvent = ev;
});
eventElement.addEventListener("pointerup",()=>{
if(timer) clearTimeout(timer);
if(timer) window.clearTimeout(timer);
timer = null;
})
eventElement.addEventListener("dblclick",clickEvent);
@@ -385,8 +482,9 @@ const createImgElement = async (
fname: fileSource,
fwidth: imgOrDiv.getAttribute("w"),
fheight: imgOrDiv.getAttribute("h"),
style: imgOrDiv.getAttribute("class"),
style: [...Array.from(imgOrDiv.classList)],
}, onCanvas);
if(!newImg) return;
parent.empty();
if(!onCanvas) {
newImg.style.maxHeight = imgMaxHeigth;
@@ -395,6 +493,20 @@ const createImgElement = async (
newImg.setAttribute("fileSource",fileSource);
parent.append(newImg);
});
const cssClasses = getFileCSSClasses(attr.file);
cssClasses.forEach((cssClass) => {
if(imgOrDiv.hasClass(cssClass)) return;
imgOrDiv.addClass(cssClass);
});
if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) {
if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.addClass("excalidraw-canvas-immersive");
}
} else {
if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.removeClass("excalidraw-canvas-immersive");
}
}
return imgOrDiv;
}
@@ -402,14 +514,16 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style, (el) => el.append(img));
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -441,11 +555,12 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
fwidth: "",
style: "",
style: [],
};
const src = internalEmbedEl.getAttribute("src");
@@ -458,11 +573,13 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
internalEmbedEl.addClass("image-embed");
attr.fwidth = internalEmbedEl.getAttribute("width")
? internalEmbedEl.getAttribute("width")
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height");
? internalEmbedEl.getAttribute("width")
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height")
? internalEmbedEl.getAttribute("height")
: getDefaultHeight(plugin);
let alt = internalEmbedEl.getAttribute("alt");
attr.style = "excalidraw-svg";
attr.style = ["excalidraw-svg"];
processAltText(src.split("#")[0],alt,attr);
const fnameParts = getEmbeddedFilenameParts(src);
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
@@ -475,36 +592,42 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
attr.fwidth = parts[2] ?? attr.fwidth;
attr.fheight = parts[3] ?? attr.fheight;
if (parts[4] && !parts[4].startsWith(fname)) {
attr.style = `excalidraw-svg${`-${parts[4]}`}`;
attr.style = [`excalidraw-svg${`-${parts[4]}`}`];
}
if (
(!parts[4] || parts[4]==="") &&
(!parts[2] || parts[2]==="") &&
parts[0] && parts[0] !== ""
) {
attr.style = `excalidraw-svg${`-${parts[0]}`}`;
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
}
}
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
}
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -521,8 +644,18 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
let internalEmbedDiv: HTMLElement = containerEl;
while (
!internalEmbedDiv.hasClass("print") &&
!internalEmbedDiv.hasClass("dataview") &&
!internalEmbedDiv.hasClass("cm-preview-code-block") &&
!internalEmbedDiv.hasClass("cm-embed-block") &&
@@ -533,7 +666,7 @@ const tmpObsidianWYSIWYG = async (
) {
internalEmbedDiv = internalEmbedDiv.parentElement;
}
if(
internalEmbedDiv.hasClass("dataview") ||
internalEmbedDiv.hasClass("cm-preview-code-block") ||
@@ -542,23 +675,50 @@ const tmpObsidianWYSIWYG = async (
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
}
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
fheight: "",
fwidth: getDefaultWidth(plugin),
style: "excalidraw-svg",
fheight: isPrinting ? "100%" : getDefaultHeight(plugin),
fwidth: isPrinting ? "100%" : getDefaultWidth(plugin),
style: ["excalidraw-svg"],
};
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view");
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
}
//We are processing the markdown preview of an actual Excalidraw file
//the excalidraw file in markdown preview mode
const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
el.empty();
if(!isFrontmatterDiv) {
let areaPreview = false;
if(Boolean(ctx.frontmatter)) {
el.empty();
} else {
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
if(warningEl) {
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
if(ref) {
attr.fname = file.path + ref;
areaPreview = true;
}
}
}
if(!isFrontmatterDiv && !areaPreview) {
if(el.parentElement === containerEl) containerEl.removeChild(el);
return;
}
@@ -600,26 +760,30 @@ const tmpObsidianWYSIWYG = async (
internalEmbedDiv.appendChild(imgDiv);
//timer to avoid the image flickering when the user is typing
let timer: NodeJS.Timeout = null;
const observer = new MutationObserver((m) => {
let timer: number = null;
const markdownObserverFn: MutationCallback = (m) => {
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
return;
}
if (timer) {
clearTimeout(timer);
window.clearTimeout(timer);
}
timer = setTimeout(async () => {
timer = window.setTimeout(async () => {
timer = null;
internalEmbedDiv.empty();
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
}, 500);
});
}
const observer = DEBUGGING
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
: new MutationObserver(markdownObserverFn);
observer.observe(internalEmbedDiv, {
attributes: true, //configure it to listen to attribute changes
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -629,12 +793,43 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
const isFrontmatter = el.hasClass("mod-frontmatter") || el.firstElementChild?.hasClass("frontmatter");
if(isPrinting && isFrontmatter) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) {
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
if (!isPreview && embeddedItems.length === 0) {
if(isFrontmatter) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId) && !el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1;
if(!isAreaGroupFrameRef) {
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -643,7 +838,7 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
if (excalidrawFile) {
if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) {
el.style.display = "none";
return;
}
@@ -666,13 +861,16 @@ export const hoverEvent = (e: any) => {
};
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
export const observer = new MutationObserver(async (m) => {
if (m.length == 0) {
const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
if (m.length === 0) {
return;
}
if (!plugin.hover.linkText) {
return;
}
if (!plugin.hover.linkText.endsWith("excalidraw")) {
return;
}
const file = metadataCache.getFirstLinkpathDest(
plugin.hover.linkText,
plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
@@ -702,16 +900,14 @@ export const observer = new MutationObserver(async (m) => {
if (!plugin.hover.linkText) {
return;
}
if (m.length != 1) {
if (m.length !== 1) {
return;
}
if (m[0].addedNodes.length != 1) {
if (m[0].addedNodes.length !== 1) {
return;
}
if (
//@ts-ignore
!m[0].addedNodes[0].classNames !=
"popover hover-popover file-embed is-loaded"
(m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover"
) {
return;
}
@@ -719,13 +915,13 @@ export const observer = new MutationObserver(async (m) => {
node.empty();
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
//I prevent the default Obsidian feature of opening the link in the native app
const img = await getIMG({
file,
fname: file.path,
fwidth: "300",
fheight: null,
style: "excalidraw-svg",
style: ["excalidraw-svg"],
});
const div = createDiv("", async (el) => {
el.appendChild(img);
@@ -742,5 +938,9 @@ export const observer = new MutationObserver(async (m) => {
});
});
node.appendChild(div);
});
};
export const legacyExcalidrawPopoverObserver = DEBUGGING
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
: new MutationObserver(legacyExcalidrawPopoverObserverFn);

View File

@@ -5,12 +5,15 @@ import {
TFile,
WorkspaceLeaf,
} from "obsidian";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./Constants";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants/constants";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
import { getIMGFilename } from "./utils/FileUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { getEA } from "src";
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
import { WeakArray } from "./utils/WeakArray";
export type ScriptIconMap = {
[key: string]: { name: string; group: string; svgString: string };
@@ -21,6 +24,7 @@ export class ScriptEngine {
private scriptPath: string;
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
public scriptIconMap: ScriptIconMap;
eaInstances = new WeakArray<ExcalidrawAutomate>();
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
@@ -29,64 +33,95 @@ export class ScriptEngine {
this.registerEventHandlers();
}
registerEventHandlers() {
const handleSvgFileChange = (path: string) => {
if (!path.endsWith(".svg")) {
return;
public removeViewEAs(view: ExcalidrawView) {
const eas = new Set<ExcalidrawAutomate>();
this.eaInstances.forEach((ea) => {
if (ea.targetView === view) {
eas.add(ea);
ea.destroy();
}
const scriptFile = app.vault.getAbstractFileByPath(
getIMGFilename(path, "md"),
);
if (scriptFile && scriptFile instanceof TFile) {
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
this.loadScript(scriptFile);
}
};
const deleteEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.unloadScript(this.getScriptName(file), file.path);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("delete", deleteEventHandler),
);
});
this.eaInstances.removeObjects(eas);
}
const createEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
public destroy() {
this.eaInstances.forEach((ea) => ea.destroy());
this.eaInstances.clear();
this.eaInstances = null;
this.scriptIconMap = null;
this.plugin = null;
this.scriptPath = null;
}
private handleSvgFileChange (path: string) {
if (!path.endsWith(".svg")) {
return;
}
const scriptFile = app.vault.getAbstractFileByPath(
getIMGFilename(path, "md"),
);
if (scriptFile && scriptFile instanceof TFile) {
this.unloadScript(this.getScriptName(scriptFile), scriptFile.path);
this.loadScript(scriptFile);
}
}
private async deleteEventHandler (file: TFile) {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.unloadScript(this.getScriptName(file), file.path);
this.handleSvgFileChange(file.path);
};
private async createEventHandler (file: TFile) {
if (!(file instanceof TFile)) {
return;
}
if (!file.path.startsWith(this.scriptPath)) {
return;
}
this.loadScript(file);
this.handleSvgFileChange(file.path);
};
private async renameEventHandler (file: TAbstractFile, oldPath: string) {
if (!(file instanceof TFile)) {
return;
}
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
const newFileIsScript = file.path.startsWith(this.scriptPath);
if (oldFileIsScript) {
this.unloadScript(this.getScriptName(oldPath), oldPath);
this.handleSvgFileChange(oldPath);
}
if (newFileIsScript) {
this.loadScript(file);
handleSvgFileChange(file.path);
};
this.plugin.registerEvent(
app.vault.on("create", createEventHandler),
);
this.handleSvgFileChange(file.path);
}
}
const renameEventHandler = async (file: TAbstractFile, oldPath: string) => {
if (!(file instanceof TFile)) {
return;
}
const oldFileIsScript = oldPath.startsWith(this.scriptPath);
const newFileIsScript = file.path.startsWith(this.scriptPath);
if (oldFileIsScript) {
this.unloadScript(this.getScriptName(oldPath), oldPath);
handleSvgFileChange(oldPath);
}
if (newFileIsScript) {
this.loadScript(file);
handleSvgFileChange(file.path);
}
};
registerEventHandlers() {
this.plugin.registerEvent(
app.vault.on("rename", renameEventHandler),
this.plugin.app.vault.on(
"delete",
(file: TFile)=>this.deleteEventHandler(file)
),
);
this.plugin.registerEvent(
this.plugin.app.vault.on(
"create",
(file: TFile)=>this.createEventHandler(file)
),
);
this.plugin.registerEvent(
this.plugin.app.vault.on(
"rename",
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
),
);
}
@@ -103,7 +138,7 @@ export class ScriptEngine {
public getListofScripts(): TFile[] {
this.scriptPath = this.plugin.settings.scriptFolderPath;
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
this.scriptPath = null;
//this.scriptPath = null;
return;
}
return app.vault
@@ -170,6 +205,7 @@ export class ScriptEngine {
(async()=>{
const script = await app.vault.read(f);
if(script) {
//remove YAML frontmatter if present
this.executeScript(view, script, scriptName,f);
}
})()
@@ -199,27 +235,28 @@ export class ScriptEngine {
const commandId = `${PLUGIN_ID}:${basename}`;
// @ts-ignore
if (!app.commands.commands[commandId]) {
if (!this.plugin.app.commands.commands[commandId]) {
return;
}
// @ts-ignore
delete app.commands.commands[commandId];
delete this.plugin.app.commands.commands[commandId];
}
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
if (!view || !script || !title) {
return;
}
this.plugin.ea.reset();
this.plugin.ea.setView(view);
this.plugin.ea.activeScript = title;
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
this.eaInstances.push(ea);
ea.activeScript = title;
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
let result = null;
//try {
result = await new AsyncFunction("ea", "utils", script)(this.plugin.ea, {
result = await new AsyncFunction("ea", "utils", script)(ea, {
inputPrompt: (
header: string,
placeholder?: string,
@@ -233,7 +270,7 @@ export class ScriptEngine {
ScriptEngine.inputPrompt(
view,
this.plugin,
app,
this.plugin.app,
header,
placeholder,
value,
@@ -250,7 +287,7 @@ export class ScriptEngine {
instructions?: Instruction[],
) =>
ScriptEngine.suggester(
app,
this.plugin.app,
displayItems,
items,
hint,
@@ -261,14 +298,13 @@ export class ScriptEngine {
/*} catch (e) {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
this.plugin.ea.activeScript = null;
}*/
return result;
}
}
private updateToolPannels() {
const leaves =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf: WorkspaceLeaf) => {
const excalidrawView = leaf.view as ExcalidrawView;
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
export const TAG_PDFEXPORT = "PDFExport";
export const TAG_MDREADINGMODE = "MDReadingMode";
export const TAG_AUTOEXPORT = "Autoexport";

408
src/constants/constants.ts Normal file
View File

@@ -0,0 +1,408 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "../types/types";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { moment } from "obsidian";
import ExcalidrawPlugin from "src/main";
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
declare const PLUGIN_VERSION:string;
export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
} as const;
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
const MD_EMBEDFILES = "## Embedded Files";
const MD_DRAWING = "## Drawing";
export const MD_EX_SECTIONS = [MD_EXCALIDRAW, MD_TEXTELEMENTS, MD_ELEMENTLINKS, MD_EMBEDFILES, MD_DRAWING];
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
declare const excalidrawLib: typeof ExcalidrawLib;
export const LOCALE = moment.locale();
export const obsidianToExcalidrawMap: { [key: string]: string } = {
'en': 'en-US',
'af': 'af-ZA', // Assuming South Africa for Afrikaans
'am': 'am-ET', // Assuming Ethiopia for Amharic
'ar': 'ar-SA',
'eu': 'eu-ES',
'be': 'be-BY', // Assuming Belarus for Belarusian
'bg': 'bg-BG',
'bn': 'bn-BD', // Assuming Bangladesh for Bengali
'ca': 'ca-ES',
'cs': 'cs-CZ',
'da': 'da-DK', // Assuming Denmark for Danish
'de': 'de-DE',
'el': 'el-GR',
'eo': 'eo-EO', // Esperanto doesn't have a country
'es': 'es-ES',
'fa': 'fa-IR',
'fi-fi': 'fi-FI',
'fr': 'fr-FR',
'gl': 'gl-ES',
'he': 'he-IL',
'hi': 'hi-IN',
'hu': 'hu-HU',
'id': 'id-ID',
'it': 'it-IT',
'ja': 'ja-JP',
'ko': 'ko-KR',
'lv': 'lv-LV',
'ml': 'ml-IN', // Assuming India for Malayalam
'ms': 'ms-MY', // Assuming Malaysia for Malay
'nl': 'nl-NL',
'no': 'nb-NO', // Using Norwegian Bokmål for Norwegian
'oc': 'oc-FR', // Assuming France for Occitan
'pl': 'pl-PL',
'pt': 'pt-PT',
'pt-BR': 'pt-BR',
'ro': 'ro-RO',
'ru': 'ru-RU',
'sr': 'sr-RS', // Assuming Serbia for Serbian
'se': 'sv-SE', // Assuming Swedish for 'se'
'sk': 'sk-SK',
'sq': 'sq-AL', // Assuming Albania for Albanian
'ta': 'ta-IN', // Assuming India for Tamil
'te': 'te-IN', // Assuming India for Telugu
'th': 'th-TH',
'tr': 'tr-TR',
'uk': 'uk-UA',
'ur': 'ur-PK', // Assuming Pakistan for Urdu
'vi': 'vi-VN',
'zh': 'zh-CN',
'zh-TW': 'zh-TW',
};
export const {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
determineFocusDistance,
intersectElementWithLine,
getCommonBoundingBox,
getMaximumGroups,
measureText,
getLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
exportToSvg,
exportToBlob,
mutateElement,
restore,
mermaidToExcalidraw,
getFontFamilyString,
getContainerElement,
refreshTextDimensions,
getCSSFontDefinition,
} = excalidrawLib;
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const DEVICE: DeviceType = {
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
isPhone: document.body.hasClass("is-phone"),
isTablet: document.body.hasClass("is-tablet"),
isMobile: document.body.hasClass("is-mobile"), //running Obsidian Mobile, need to also check isTablet
isLinux: document.body.hasClass("mod-linux") && ! document.body.hasClass("is-android"),
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
isWindows: document.body.hasClass("mod-windows"),
isIOS: document.body.hasClass("is-ios"),
isAndroid: document.body.hasClass("is-android"),
};
export const ROOTELEMENTSIZE = (() => {
const tempElement = document.createElement('div');
tempElement.style.fontSize = '1rem';
tempElement.style.display = 'none'; // Hide the element
document.body.appendChild(tempElement);
const computedStyle = getComputedStyle(tempElement);
const pixelSize = parseFloat(computedStyle.fontSize);
document.body.removeChild(tempElement);
return pixelSize;
})();
export const nanoid = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
8,
);
export const KEYCODE = {
ESC: 27,
};
export const ROUNDNESS = { //should at one point publish @zsviczian/excalidraw/types/constants
LEGACY: 1,
PROPORTIONAL_RADIUS: 2,
ADAPTIVE_RADIUS: 3,
} as const;
export const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
export const GITHUB_RELEASES = "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/";
export const URLFETCHTIMEOUT = 3000;
export const PLUGIN_ID = "obsidian-excalidraw-plugin";
export const SCRIPT_INSTALL_CODEBLOCK = "excalidraw-script-install";
export const SCRIPT_INSTALL_FOLDER = "Downloaded";
export const fileid = customAlphabet("1234567890abcdef", 40);
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*#]/g;
//taken from Obsidian source code
export const REG_SECTION_REF_CLEAN = /([:#|^\\\r\n]|%%|\[\[|]])/g;
export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depricated?:boolean}} = {
"plugin": {name: "excalidraw-plugin", type: "text"},
"export-transparent": {name: "excalidraw-export-transparent", type: "checkbox"},
"mask": {name: "excalidraw-mask", type: "checkbox"},
"export-dark": {name: "excalidraw-export-dark", type: "checkbox"},
"export-svgpadding": {name: "excalidraw-export-svgpadding", type: "number", depricated: true},
"export-padding": {name: "excalidraw-export-padding", type: "number"},
"export-pngscale": {name: "excalidraw-export-pngscale", type: "number"},
"export-embed-scene": {name: "excalidraw-export-embed-scene", type: "checkbox"},
"link-prefix": {name: "excalidraw-link-prefix", type: "text"},
"url-prefix": {name: "excalidraw-url-prefix", type: "text"},
"link-brackets": {name: "excalidraw-link-brackets", type: "checkbox"},
"onload-script": {name: "excalidraw-onload-script", type: "text"},
"linkbutton-opacity": {name: "excalidraw-linkbutton-opacity", type: "number"},
"default-mode": {name: "excalidraw-default-mode", type: "text"},
"font": {name: "excalidraw-font", type: "text"},
"font-color": {name: "excalidraw-font-color", type: "text"},
"border-color": {name: "excalidraw-border-color", type: "text"},
"md-css": {name: "excalidraw-css", type: "text"},
"autoexport": {name: "excalidraw-autoexport", type: "text"},
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
};
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const ICON_NAME = "excalidraw-icon";
export const MAX_COLORS = 5;
export const COLOR_FREQ = 6;
export const RERENDER_EVENT = "excalidraw-embed-rerender";
export const BLANK_DRAWING =
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
export const DARK_BLANK_DRAWING =
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
export const FRONTMATTER = [
"---",
"",
`${FRONTMATTER_KEYS["plugin"].name}: parsed`,
"tags: [excalidraw]",
"",
"---",
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: 'Decompress current Excalidraw file'. For more info check in plugin settings under 'Saving'",
"",
"",
].join("\n");
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
/*export const FULLSCREEN_ICON_NAME = "fullscreen";
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";*/
export const SCRIPTENGINE_ICON_NAME = "ScriptEngine";
export const KEYBOARD_EVENT_TYPES = [
"keydown",
"keyup",
"keypress"
];
export const EXTENDED_EVENT_TYPES = [
/* "pointerdown",
"pointerup",
"pointermove",
"mousedown",
"mouseup",
"mousemove",
"mouseover",
"mouseout",
"mouseenter",
"mouseleave",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",*/
"copy",
"cut",
"paste",
/*"wheel",
"touchstart",
"touchend",
"touchmove",*/
];
//export const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
export const COLOR_NAMES = new Map<string, string>();
COLOR_NAMES.set("aliceblue", "#f0f8ff");
COLOR_NAMES.set("antiquewhite", "#faebd7");
COLOR_NAMES.set("aqua", "#00ffff");
COLOR_NAMES.set("aquamarine", "#7fffd4");
COLOR_NAMES.set("azure", "#f0ffff");
COLOR_NAMES.set("beige", "#f5f5dc");
COLOR_NAMES.set("bisque", "#ffe4c4");
COLOR_NAMES.set("black", "#000000");
COLOR_NAMES.set("blanchedalmond", "#ffebcd");
COLOR_NAMES.set("blue", "#0000ff");
COLOR_NAMES.set("blueviolet", "#8a2be2");
COLOR_NAMES.set("brown", "#a52a2a");
COLOR_NAMES.set("burlywood", "#deb887");
COLOR_NAMES.set("cadetblue", "#5f9ea0");
COLOR_NAMES.set("chartreuse", "#7fff00");
COLOR_NAMES.set("chocolate", "#d2691e");
COLOR_NAMES.set("coral", "#ff7f50");
COLOR_NAMES.set("cornflowerblue", "#6495ed");
COLOR_NAMES.set("cornsilk", "#fff8dc");
COLOR_NAMES.set("crimson", "#dc143c");
COLOR_NAMES.set("cyan", "#00ffff");
COLOR_NAMES.set("darkblue", "#00008b");
COLOR_NAMES.set("darkcyan", "#008b8b");
COLOR_NAMES.set("darkgoldenrod", "#b8860b");
COLOR_NAMES.set("darkgray", "#a9a9a9");
COLOR_NAMES.set("darkgreen", "#006400");
COLOR_NAMES.set("darkkhaki", "#bdb76b");
COLOR_NAMES.set("darkmagenta", "#8b008b");
COLOR_NAMES.set("darkolivegreen", "#556b2f");
COLOR_NAMES.set("darkorange", "#ff8c00");
COLOR_NAMES.set("darkorchid", "#9932cc");
COLOR_NAMES.set("darkred", "#8b0000");
COLOR_NAMES.set("darksalmon", "#e9967a");
COLOR_NAMES.set("darkseagreen", "#8fbc8f");
COLOR_NAMES.set("darkslateblue", "#483d8b");
COLOR_NAMES.set("darkslategray", "#2f4f4f");
COLOR_NAMES.set("darkturquoise", "#00ced1");
COLOR_NAMES.set("darkviolet", "#9400d3");
COLOR_NAMES.set("deeppink", "#ff1493");
COLOR_NAMES.set("deepskyblue", "#00bfff");
COLOR_NAMES.set("dimgray", "#696969");
COLOR_NAMES.set("dodgerblue", "#1e90ff");
COLOR_NAMES.set("firebrick", "#b22222");
COLOR_NAMES.set("floralwhite", "#fffaf0");
COLOR_NAMES.set("forestgreen", "#228b22");
COLOR_NAMES.set("fuchsia", "#ff00ff");
COLOR_NAMES.set("gainsboro", "#dcdcdc");
COLOR_NAMES.set("ghostwhite", "#f8f8ff");
COLOR_NAMES.set("gold", "#ffd700");
COLOR_NAMES.set("goldenrod", "#daa520");
COLOR_NAMES.set("gray", "#808080");
COLOR_NAMES.set("green", "#008000");
COLOR_NAMES.set("greenyellow", "#adff2f");
COLOR_NAMES.set("honeydew", "#f0fff0");
COLOR_NAMES.set("hotpink", "#ff69b4");
COLOR_NAMES.set("indianred", "#cd5c5c");
COLOR_NAMES.set("indigo", "#4b0082");
COLOR_NAMES.set("ivory", "#fffff0");
COLOR_NAMES.set("khaki", "#f0e68c");
COLOR_NAMES.set("lavender", "#e6e6fa");
COLOR_NAMES.set("lavenderblush", "#fff0f5");
COLOR_NAMES.set("lawngreen", "#7cfc00");
COLOR_NAMES.set("lemonchiffon", "#fffacd");
COLOR_NAMES.set("lightblue", "#add8e6");
COLOR_NAMES.set("lightcoral", "#f08080");
COLOR_NAMES.set("lightcyan", "#e0ffff");
COLOR_NAMES.set("lightgoldenrodyellow", "#fafad2");
COLOR_NAMES.set("lightgrey", "#d3d3d3");
COLOR_NAMES.set("lightgreen", "#90ee90");
COLOR_NAMES.set("lightpink", "#ffb6c1");
COLOR_NAMES.set("lightsalmon", "#ffa07a");
COLOR_NAMES.set("lightseagreen", "#20b2aa");
COLOR_NAMES.set("lightskyblue", "#87cefa");
COLOR_NAMES.set("lightslategray", "#778899");
COLOR_NAMES.set("lightsteelblue", "#b0c4de");
COLOR_NAMES.set("lightyellow", "#ffffe0");
COLOR_NAMES.set("lime", "#00ff00");
COLOR_NAMES.set("limegreen", "#32cd32");
COLOR_NAMES.set("linen", "#faf0e6");
COLOR_NAMES.set("magenta", "#ff00ff");
COLOR_NAMES.set("maroon", "#800000");
COLOR_NAMES.set("mediumaquamarine", "#66cdaa");
COLOR_NAMES.set("mediumblue", "#0000cd");
COLOR_NAMES.set("mediumorchid", "#ba55d3");
COLOR_NAMES.set("mediumpurple", "#9370d8");
COLOR_NAMES.set("mediumseagreen", "#3cb371");
COLOR_NAMES.set("mediumslateblue", "#7b68ee");
COLOR_NAMES.set("mediumspringgreen", "#00fa9a");
COLOR_NAMES.set("mediumturquoise", "#48d1cc");
COLOR_NAMES.set("mediumvioletred", "#c71585");
COLOR_NAMES.set("midnightblue", "#191970");
COLOR_NAMES.set("mintcream", "#f5fffa");
COLOR_NAMES.set("mistyrose", "#ffe4e1");
COLOR_NAMES.set("moccasin", "#ffe4b5");
COLOR_NAMES.set("navajowhite", "#ffdead");
COLOR_NAMES.set("navy", "#000080");
COLOR_NAMES.set("oldlace", "#fdf5e6");
COLOR_NAMES.set("olive", "#808000");
COLOR_NAMES.set("olivedrab", "#6b8e23");
COLOR_NAMES.set("orange", "#ffa500");
COLOR_NAMES.set("orangered", "#ff4500");
COLOR_NAMES.set("orchid", "#da70d6");
COLOR_NAMES.set("palegoldenrod", "#eee8aa");
COLOR_NAMES.set("palegreen", "#98fb98");
COLOR_NAMES.set("paleturquoise", "#afeeee");
COLOR_NAMES.set("palevioletred", "#d87093");
COLOR_NAMES.set("papayawhip", "#ffefd5");
COLOR_NAMES.set("peachpuff", "#ffdab9");
COLOR_NAMES.set("peru", "#cd853f");
COLOR_NAMES.set("pink", "#ffc0cb");
COLOR_NAMES.set("plum", "#dda0dd");
COLOR_NAMES.set("powderblue", "#b0e0e6");
COLOR_NAMES.set("purple", "#800080");
COLOR_NAMES.set("rebeccapurple", "#663399");
COLOR_NAMES.set("red", "#ff0000");
COLOR_NAMES.set("rosybrown", "#bc8f8f");
COLOR_NAMES.set("royalblue", "#4169e1");
COLOR_NAMES.set("saddlebrown", "#8b4513");
COLOR_NAMES.set("salmon", "#fa8072");
COLOR_NAMES.set("sandybrown", "#f4a460");
COLOR_NAMES.set("seagreen", "#2e8b57");
COLOR_NAMES.set("seashell", "#fff5ee");
COLOR_NAMES.set("sienna", "#a0522d");
COLOR_NAMES.set("silver", "#c0c0c0");
COLOR_NAMES.set("skyblue", "#87ceeb");
COLOR_NAMES.set("slateblue", "#6a5acd");
COLOR_NAMES.set("slategray", "#708090");
COLOR_NAMES.set("snow", "#fffafa");
COLOR_NAMES.set("springgreen", "#00ff7f");
COLOR_NAMES.set("steelblue", "#4682b4");
COLOR_NAMES.set("tan", "#d2b48c");
COLOR_NAMES.set("teal", "#008080");
COLOR_NAMES.set("thistle", "#d8bfd8");
COLOR_NAMES.set("tomato", "#ff6347");
COLOR_NAMES.set("turquoise", "#40e0d0");
COLOR_NAMES.set("violet", "#ee82ee");
COLOR_NAMES.set("wheat", "#f5deb3");
COLOR_NAMES.set("white", "#ffffff");
COLOR_NAMES.set("whitesmoke", "#f5f5f5");
COLOR_NAMES.set("yellow", "#ffff00");
COLOR_NAMES.set("yellowgreen", "#9acd32");
export const DEFAULT_MD_EMBED_CSS = `.snw-reference{display: none;}.excalidraw-md-host{padding:0px 10px}.excalidraw-md-footer{height:5px}foreignObject{background-color:transparent}p{display:block;margin-block-start:1em;margin-block-end:1em;margin-inline-start:0px;margin-inline-end:0px;color:inherit}table,tr,th,td{color:inherit;border:1px solid;border-collapse:collapse;padding:3px}th{font-weight:bold;border-bottom:double;background-color:silver}.copy-code-button{display:none}code[class*=language-],pre[class*=language-]{color:#393a34;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;font-size:.9em;line-height:1.2em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre>code[class*=language-]{font-size:1em}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{background:#C1DEF1}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{background:#C1DEF1}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;background-color:#0000001a}:not(pre)>code[class*=language-]{padding:.2em;padding-top:1px;padding-bottom:1px;background:#f8f8f8;border:1px solid #dddddd}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:green;font-style:italic}.token.namespace{opacity:.7}.token.string{color:#a31515}.token.punctuation,.token.operator{color:#393a34}.token.url,.token.symbol,.token.number,.token.boolean,.token.variable,.token.constant,.token.inserted{color:#36acaa}.token.atrule,.token.keyword,.token.attr-value,.language-autohotkey .token.selector,.language-json .token.boolean,.language-json .token.number,code[class*=language-css]{color:#00f}.token.function{color:#393a34}.token.deleted,.language-autohotkey .token.tag{color:#9a050f}.token.selector,.language-autohotkey .token.keyword{color:#00009f}.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.class-name,.language-json .token.property{color:#2b91af}.token.tag,.token.selector{color:maroon}.token.attr-name,.token.property,.token.regex,.token.entity{color:red}.token.directive.tag .tag{background:#ffff00;color:#393a34}.line-numbers.line-numbers .line-numbers-rows{border-right-color:#a5a5a5}.line-numbers .line-numbers-rows>span:before{color:#2b91af}.line-highlight.line-highlight{background:rgba(193,222,241,.2);background:-webkit-linear-gradient(left,rgba(193,222,241,.2) 70%,rgba(221,222,241,0));background:linear-gradient(to right,rgba(193,222,241,.2) 70%,rgba(221,222,241,0))}blockquote{ font-style:italic;background-color:rgb(46,43,42,0.1);margin:0;margin-left:1em;border-radius:0 4px 4px 0;border:1px solid hsl(0,80%,32%);border-left-width:8px;border-top-width:0px;border-right-width:0px;border-bottom-width:0px;padding:10px 20px;margin-inline-start:30px;margin-inline-end:30px;}`;
export const SCRIPTENGINE_ICON = `<g transform="translate(-8,-8)"><path d="M24.318 37.983c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749m.126-.104c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749" fill="none" stroke-width="2" stroke-linecap="round" stroke="currentColor"/><path d="M81.235 56.502a23.3 23.3 0 0 1-1.46 8.068 20.785 20.785 0 0 1-1.762 3.72 24.068 24.068 0 0 1-5.337 6.26 22.575 22.575 0 0 1-3.449 2.358 23.726 23.726 0 0 1-7.803 2.803 24.719 24.719 0 0 1-8.333 0 24.102 24.102 0 0 1-4.028-1.074 23.71 23.71 0 0 1-3.776-1.729 23.259 23.259 0 0 1-6.369-5.265 23.775 23.775 0 0 1-2.416-3.353 24.935 24.935 0 0 1-1.762-3.72 23.765 23.765 0 0 1-1.083-3.981 23.454 23.454 0 0 1 0-8.173c.252-1.336.604-2.698 1.083-3.956a24.935 24.935 0 0 1 1.762-3.72 22.587 22.587 0 0 1 2.416-3.378c.881-1.048 1.888-2.017 2.946-2.908a24.38 24.38 0 0 1 3.423-2.357 23.71 23.71 0 0 1 3.776-1.73 21.74 21.74 0 0 1 4.028-1.047 23.437 23.437 0 0 1 8.333 0 24.282 24.282 0 0 1 7.803 2.777 26.198 26.198 0 0 1 3.45 2.357 24.62 24.62 0 0 1 5.336 6.287 20.785 20.785 0 0 1 1.762 3.72 21.32 21.32 0 0 1 1.083 3.955c.251 1.336.302 3.405.377 4.086.05.681.05-.68 0 0" fill="none" stroke-width="4" stroke-linecap="round" stroke="currentColor"/><path d="M69.404 56.633c-6.596-3.3-13.216-6.6-19.51-9.744m19.51 9.744c-6.747-3.379-13.493-6.758-19.51-9.744m0 0v19.489m0-19.49v19.49m0 0c4.355-2.148 8.71-4.322 19.51-9.745m-19.51 9.745c3.978-1.965 7.93-3.956 19.51-9.745m0 0h0m0 0h0" fill="currentColor" stroke-linecap="round" stroke="currentColor" stroke-width="4"/></g>`;
export const DISK_ICON_NAME = "save";
export const EXPORT_IMG_ICON = ` <g transform="scale(4.166)" strokeWidth="1.25" fill="none" stroke="currentColor"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 8h.01"></path><path d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"></path><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"></path><path d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"></path><path d="M19 16v6"></path><path d="M22 19l-3 3l-3 -3"></path></g>`;
export const EXPORT_IMG_ICON_NAME = `export-img`;
export const EXCALIDRAW_ICON = `<path d="M24 17h121v121H24z" style="fill:none" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"/><path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01Zm-66.93-65.3c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02Zm78.54-1.18c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:currentColor;fill-rule:nonzero" transform="translate(-26.41 -29.49)"/>`;

View File

@@ -0,0 +1,116 @@
/*
#exclude
```js*/
/**
* If set, this callback is triggered when the user closes an Excalidraw view.
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
*/
//ea.onViewUnloadHook = (view) => {};
/**
* If set, this callback is triggered, when the user changes the view mode.
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
*/
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
/**
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
* onLinkHoverHook: (
* element: NonDeletedExcalidrawElement,
* linkText: string,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
/**
* If set, this callback is triggered, when the user clicks a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
* onLinkClickHook:(
* element: ExcalidrawElement,
* linkText: string,
* event: MouseEvent,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
* You can use this callback in case you want to do something additional when the onDrop event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
* onDropHook: (data: {
* ea: ExcalidrawAutomate;
* event: React.DragEvent<HTMLDivElement>;
* 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 = null;
*/
//ea.onDropHook = (data) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
* onPasteHook: (data: {
* ea: ExcalidrawAutomate;
* payload: ClipboardData;
* event: ClipboardEvent;
* excalidrawFile: TFile; //the file receiving the paste event
* view: ExcalidrawView; //the excalidraw view receiving the paste
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
* }) => boolean = null;
*/
//ea.onPasteHook = (data) => {};
/**
* if set, this callback is triggered, when an Excalidraw file is opened
* You can use this callback in case you want to do something additional when the file is opened.
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
* onFileOpenHook: (data: {
* ea: ExcalidrawAutomate;
* excalidrawFile: TFile; //the file being loaded
* view: ExcalidrawView;
* }) => Promise<void>;
*/
//ea.onFileOpenHook = (data) => {};
/**
* if set, this callback is triggered, when an Excalidraw file is created
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
* onFileCreateHook: (data: {
* ea: ExcalidrawAutomate;
* excalidrawFile: TFile; //the file being created
* view: ExcalidrawView;
* }) => Promise<void>;
*/
//ea.onFileCreateHook = (data) => {};
/**
* If set, this callback is triggered whenever the active canvas color changes
* onCanvasColorChangeHook: (
* ea: ExcalidrawAutomate,
* view: ExcalidrawView, //the excalidraw view
* color: string,
* ) => void = null;
*/
//ea.onCanvasColorChangeHook = (ea, view, color) => {};

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,13 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import ExcalidrawView from "./ExcalidrawView";
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./Constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
declare module "obsidian" {
interface Workspace {
@@ -18,13 +19,24 @@ declare module "obsidian" {
}
}
function getTheme (view: ExcalidrawView, theme:string): string {
return view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
}
//--------------------------------------------------------------------------------
//Render webview for anything other than Vimeo and Youtube
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
//required to control the video
//--------------------------------------------------------------------------------
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
if(DEVICE.isDesktop) {
export function renderWebView (src: string, view: ExcalidrawView, id: string, _: UIAppState):JSX.Element {
const isDataURL = src.startsWith("data:");
if(DEVICE.isDesktop && !isDataURL) {
return (
<webview
ref={(ref) => view.updateEmbeddableRef(id, ref)}
@@ -46,11 +58,12 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
title="Excalidraw Embedded Content"
allowFullScreen={true}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
src={src}
src={isDataURL ? null : src}
style={{
overflow: "hidden",
borderRadius: "var(--embeddable-radius)",
}}
srcDoc={isDataURL ? atob(src.split(',')[1]) : null}
/>
);
}
@@ -59,13 +72,15 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
//Render WorkspaceLeaf or CanvasNode
//--------------------------------------------------------------------------------
function RenderObsidianView(
{ element, linkText, view, containerRef, appState, theme }:{
{ mdProps, element, linkText, view, containerRef, activeEmbeddable, theme, canvasColor }:{
mdProps: EmbeddableMDCustomProps;
element: NonDeletedExcalidrawElement;
linkText: string;
view: ExcalidrawView;
containerRef: React.RefObject<HTMLDivElement>;
appState: UIAppState;
activeEmbeddable: {element: NonDeletedExcalidrawElement; state: string};
theme: string;
canvasColor: string;
}): JSX.Element {
const { subpath, file } = processLinkText(linkText, view);
@@ -73,25 +88,36 @@ function RenderObsidianView(
if (!file) {
return null;
}
const react = view.plugin.getPackage(view.ownerWindow).react;
const React = view.packages.react;
//@ts-ignore
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const leafRef = React.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
const isEditingRef = React.useRef(false);
const isActiveRef = React.useRef(false);
const themeRef = React.useRef(theme);
const elementRef = React.useRef(element);
// Update themeRef when theme changes
React.useEffect(() => {
themeRef.current = theme;
}, [theme]);
// Update elementRef when element changes
React.useEffect(() => {
elementRef.current = element;
}, [element]);
//--------------------------------------------------------------------------------
//block propagation of events to the parent if the iframe element is active
//--------------------------------------------------------------------------------
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
const stopPropagation = React.useCallback((event:React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation(); // Stop the event from propagating up the DOM tree
}
}, [isActiveRef.current]);
//runs once after mounting of the component and when the component is unmounted
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current) {
return;
}
@@ -110,7 +136,7 @@ function RenderObsidianView(
}, []);
//blocking or not the propagation of events to the parent if the iframe is active
react.useEffect(() => {
React.useEffect(() => {
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
if(!containerRef?.current) {
return;
@@ -130,9 +156,9 @@ function RenderObsidianView(
//--------------------------------------------------------------------------------
//mount the workspace leaf or the canvas node depending on subpath
//Mount the workspace leaf or the canvas node depending on subpath
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current) {
return;
}
@@ -141,6 +167,8 @@ function RenderObsidianView(
containerRef.current.removeChild(containerRef.current.lastChild);
}
containerRef.current.parentElement.style.padding = "";
const doc = view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
@@ -149,8 +177,9 @@ function RenderObsidianView(
rootSplit.containerEl.style.height = '100%';
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
leafRef.current = {
leaf: app.workspace.createLeafInParent(rootSplit, 0),
node: null
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
node: null,
editNode: null,
};
const setKeepOnTop = () => {
@@ -190,35 +219,128 @@ function RenderObsidianView(
//This runs only when the file is added, thus should not be a major performance issue
await leafRef.current.leaf.setViewState({state: {file:null}})
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
setColors(containerRef.current, element, mdProps, canvasColor);
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
rootSplit.containerEl.addClass("mod-visible");
containerRef.current.appendChild(rootSplit.containerEl);
setColors(containerRef.current, element, mdProps, canvasColor);
}
patchMobileView(view);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
})();
}
return () => {}; //cleanup on unmount
return () => {
if(!leafRef.current) {
return;
}
view.canvasNodeFactory.removeNode(leafRef.current.node);
leafRef.current.leaf?.detach();
leafRef.current = null;
}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
react.useEffect(() => {
//--------------------------------------------------------------------------------
//Set colors of the canvas node
//--------------------------------------------------------------------------------
function setColors (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
const canvasNodeContainer = containerRef.current?.firstElementChild as HTMLElement;
if(mdProps.useObsidianDefaults) {
canvasNode?.style.removeProperty("--canvas-background");
canvasNodeContainer?.style.removeProperty("background-color");
canvasNode?.style.removeProperty("--canvas-border");
canvasNodeContainer?.style.removeProperty("border-color");
return;
}
const ea = view.plugin.ea;
if(mdProps.backgroundMatchElement) {
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
const color = element?.backgroundColor
? (element.backgroundColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
: "transparent";
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
const opacity = (mdProps.backgroundOpacity??100)/100;
const color = mdProps.backgroundMatchCanvas
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
}
if(mdProps.borderMatchElement) {
const opacity = (mdProps?.borderOpacity ?? 50)/100;
const color = element?.strokeColor
? (element.strokeColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
}
}
//--------------------------------------------------------------------------------
//Set colors of the canvas node
//--------------------------------------------------------------------------------
React.useEffect(() => {
if(!containerRef.current) {
return;
}
const element = elementRef.current;
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
}, [
mdProps,
elementRef.current,
containerRef.current,
canvasColor,
])
//--------------------------------------------------------------------------------
//Switch to preview mode when the iframe is not active
//--------------------------------------------------------------------------------
React.useEffect(() => {
if(isEditingRef.current) {
if(leafRef.current?.node) {
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
isEditingRef.current = false;
}
}, [isEditingRef.current, leafRef]);
//--------------------------------------------------------------------------------
//Switch to edit mode when markdown view is clicked
//--------------------------------------------------------------------------------
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
const handleClick = React.useCallback((event?: React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation();
event?.stopPropagation();
}
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
@@ -240,21 +362,39 @@ function RenderObsidianView(
patchMobileView(view);
} else if (leafRef.current?.node) {
//Handle canvas node
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
}
}
}, [leafRef.current?.leaf, element.id]);
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
if(leafRef.current) leafRef.current.editNode = handleClick;
// Event listener for key press
React.useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Enter") {
handleClick(event); // Call handleClick function when Enter key is pressed
}
};
document.addEventListener("keydown", handleKeyPress); // Add event listener for key press
return () => {
document.removeEventListener("keydown", handleKeyPress); // Remove event listener when component unmounts
};
}, [handleClick]);
//--------------------------------------------------------------------------------
// Set isActiveRef and switch to preview mode when the iframe is not active
//--------------------------------------------------------------------------------
react.useEffect(() => {
React.useEffect(() => {
if(!containerRef?.current || !leafRef?.current) {
return;
}
const previousIsActive = isActiveRef.current;
isActiveRef.current = (appState.activeEmbeddable?.element.id === element.id) && (appState.activeEmbeddable?.state === "active");
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
if (previousIsActive === isActiveRef.current) {
return;
@@ -276,38 +416,37 @@ function RenderObsidianView(
}
} else if (leafRef.current?.node) {
//Handle canvas node
view.canvasNodeFactory.stopEditing(leafRef.current.node);
if(view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) {
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
} else {
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}
}, [
containerRef,
leafRef,
isActiveRef,
appState.activeEmbeddable?.element,
appState.activeEmbeddable?.state,
activeEmbeddable?.element,
activeEmbeddable?.state,
element,
view,
linkText,
subpath,
file,
theme,
isEditingRef,
view.canvasNodeFactory
view.canvasNodeFactory,
themeRef.current
]);
return null;
};
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const theme = view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? appState.theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const React = view.packages.react;
const containerRef: React.RefObject<HTMLDivElement> = React.useRef(null);
const theme = getTheme(view, appState.theme);
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
return (
<div
ref={containerRef}
@@ -317,15 +456,19 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
borderRadius: "var(--embeddable-radius)",
color: `var(--text-normal)`,
}}
className={theme}
className={`${theme} canvas-node ${
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
>
<RenderObsidianView
mdProps={mdProps}
element={element}
linkText={linkText}
view={view}
containerRef={containerRef}
appState={appState}
theme={theme}/>
activeEmbeddable={appState.activeEmbeddable}
theme={appState.theme}
canvasColor={appState.viewBackgroundColor}
/>
</div>
)
}

View File

@@ -0,0 +1,179 @@
import { Setting, ToggleComponent } from "obsidian";
import { EmbeddableMDCustomProps } from "./EmbeddableSettings";
import { fragWithHTML } from "src/utils/Utils";
import { t } from "src/lang/helpers";
export class EmbeddalbeMDFileCustomDataSettingsComponent {
constructor (
private contentEl: HTMLElement,
private mdCustomData: EmbeddableMDCustomProps,
private update?: Function,
private isMDFile: boolean = true,
) {
if(!update) this.update = () => {};
}
render() {
let detailsDIV: HTMLDivElement;
new Setting(this.contentEl)
.setName(t("ES_USE_OBSIDIAN_DEFAULTS"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.useObsidianDefaults)
.onChange(value => {
this.mdCustomData.useObsidianDefaults = value;
detailsDIV.style.display = value ? "none" : "block";
this.update();
})
);
this.contentEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsDIV = this.contentEl.createDiv();
detailsDIV.style.display = this.mdCustomData.useObsidianDefaults ? "none" : "block";
const contentEl = detailsDIV
if(this.isMDFile) {
new Setting(contentEl)
.setName(t("ES_FILENAME_VISIBLE"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.filenameVisible)
.onChange(value => {
this.mdCustomData.filenameVisible = value;
})
);
}
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;
let bgMatchCanvasToggle: ToggleComponent;
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_ELEMENT"))
.addToggle(toggle => {
bgMatchElementToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchElement)
.onChange(value => {
this.mdCustomData.backgroundMatchElement = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchCanvas) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_CANVAS"))
.addToggle(toggle => {
bgMatchCanvasToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchCanvas)
.onChange(value => {
this.mdCustomData.backgroundMatchCanvas = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchElement) {
bgMatchElementToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchElement) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
if(this.mdCustomData.backgroundMatchElement && this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
bgSetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.backgroundColor)
.onChange((value) => {
this.mdCustomData.backgroundColor = value;
this.update();
})
);
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
const opacity = (value:number):DocumentFragment => {
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
}
const bgOpacitySetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_OPACITY"))
.setDesc(opacity(this.mdCustomData.backgroundOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.backgroundOpacity)
.onChange(value => {
this.mdCustomData.backgroundOpacity = value;
bgOpacitySetting.setDesc(opacity(value));
this.update();
})
);
if(this.isMDFile) {
contentEl.createEl("h4",{text: t("ES_BORDER_HEAD")});
let borderSetting: Setting;
new Setting(contentEl)
.setName(t("ES_BORDER_MATCH_ELEMENT"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.borderMatchElement)
.onChange(value => {
this.mdCustomData.borderMatchElement = value;
if(value) {
borderSetting.settingEl.style.display = "none";
} else {
borderSetting.settingEl.style.display = "";
}
this.update();
})
);
borderSetting = new Setting(contentEl)
.setName(t("ES_BORDER_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.borderColor)
.onChange((value) => {
this.mdCustomData.borderColor = value;
this.update();
})
);
borderSetting.settingEl.style.display = this.mdCustomData.borderMatchElement ? "none" : "";
const borderOpacitySetting = new Setting(contentEl)
.setName(t("ES_BORDER_OPACITY"))
.setDesc(opacity(this.mdCustomData.borderOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.borderOpacity)
.onChange(value => {
this.mdCustomData.borderOpacity = value;
borderOpacitySetting.setDesc(opacity(value));
this.update();
})
);
}
}
}

View File

@@ -0,0 +1,237 @@
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { Modal, Notice, Setting, TFile, ToggleComponent } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename } from "src/utils/FileUtils";
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
import { isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
export type EmbeddableMDCustomProps = {
useObsidianDefaults: boolean;
backgroundMatchCanvas: boolean;
backgroundMatchElement: boolean;
backgroundColor: string;
backgroundOpacity: number;
borderMatchElement: boolean;
borderColor: string;
borderOpacity: number;
filenameVisible: boolean;
}
export class EmbeddableSettings extends Modal {
private ea: ExcalidrawAutomate;
private updatedFilepath: string = null;
private zoomValue: number;
private isYouTube: boolean;
private youtubeStart: string = null;
private isMDFile: boolean;
private notExcalidrawIsInternal: boolean;
private isLocalURI: boolean;
private mdCustomData: EmbeddableMDCustomProps;
private onKeyDown: (ev: KeyboardEvent) => void;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
private file: TFile,
private element: ExcalidrawEmbeddableElement
) {
super(plugin.app);
this.ea = getEA(this.view);
this.ea.copyViewElementsToEAforEditing([this.element]);
this.zoomValue = element.scale[0];
this.isYouTube = isYouTube(this.element.link);
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
this.isMDFile = this.file && this.file.extension === "md"; // && !this.view.plugin.isExcalidrawFile(this.file);
this.isLocalURI = this.element.link.startsWith("file://");
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
this.mdCustomData = element.customData?.mdProps ?? view.plugin.settings.embeddableMarkdownDefaults;
if(!element.customData?.mdProps) {
const bgCM = this.plugin.ea.getCM(element.backgroundColor);
this.mdCustomData.backgroundColor = bgCM.stringHEX({alpha: false});
this.mdCustomData.backgroundOpacity = element.opacity;
const borderCM = this.plugin.ea.getCM(element.strokeColor);
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
this.mdCustomData.borderOpacity = element.opacity;
}
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
//this.titleEl.setText(t("ES_TITLE"));
this.createForm();
}
onClose() {
this.containerEl.removeEventListener("keydown",this.onKeyDown);
this.plugin = null;
this.view = null;
this.file = null;
this.element = null;
this.ea.destroy();
this.ea = null;
this.mdCustomData = null;
}
async createForm() {
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
if(this.file) {
new Setting(this.contentEl)
.setName(t("ES_RENAME"))
.addText(text =>
text
.setValue(getPathWithoutExtension(this.file))
.onChange(async (value) => {
this.updatedFilepath = value;
})
)
}
const zoomValue = ():DocumentFragment => {
return fragWithHTML(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
}
const zoomSetting = new Setting(this.contentEl)
.setName(t("ES_ZOOM"))
.setDesc(zoomValue())
.addButton(button =>
button
.setButtonText(t("ES_ZOOM_100"))
.onClick(() => {
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
this.zoomValue = 1/api.getAppState().zoom.value;
zoomSetting.setDesc(zoomValue());
})
)
.addSlider(slider =>
slider
.setLimits(10,400,5)
.setValue(this.zoomValue*100)
.onChange(value => {
this.zoomValue = value/100;
zoomSetting.setDesc(zoomValue());
})
)
if(this.isYouTube) {
new Setting(this.contentEl)
.setName(t("ES_YOUTUBE_START"))
.setDesc(t("ES_YOUTUBE_START_DESC"))
.addText(text =>
text
.setValue(this.youtubeStart)
.onChange(async (value) => {
this.youtubeStart = value;
})
)
}
if(this.isMDFile || this.notExcalidrawIsInternal) {
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData, undefined, this.isMDFile).render();
}
new Setting(this.contentEl)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
.setTooltip("ESC")
.onClick(this.close.bind(this))
)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_OK"))
.setTooltip("CTRL/Opt+Enter")
.setCta()
.onClick(this.applySettings.bind(this))
)
const onKeyDown = (ev: KeyboardEvent) => {
if(isWinCTRLorMacCMD(ev) && ev.key === "Enter") {
this.applySettings();
}
}
this.onKeyDown = onKeyDown;
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
}
private async applySettings() {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
if(this.updatedFilepath) {
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
if(newPathWithExt !== this.file.path) {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.filename,
fnparts.folderpath,
);
if(this.app.vault.getAbstractFileByPath(newPath)) {
new Notice("File rename failed. A file with this name already exists.\n"+newPath,10000);
} else {
try {
await this.app.fileManager.renameFile(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
} catch(e) {
new Notice("File rename failed. "+e,10000);
}
}
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
dirty = true;
if(this.youtubeStart === "" || isValidYouTubeStart(this.youtubeStart)) {
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
} else {
new Notice(t("ES_YOUTUBE_START_INVALID"));
}
}
if(
this.isMDFile && (
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
) {
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
dirty = true;
}
if(this.zoomValue !== this.element.scale[0]) {
dirty = true;
el.scale = [this.zoomValue,this.zoomValue];
}
if(dirty) {
(async() => {
await this.ea.addElementsToView();
//@ts-ignore
this.ea.viewUpdateScene({appState: {}, storeAction: "update"});
this.close(); //close should only run once update scene is done
})();
} else {
this.close();
}
};
}

View File

@@ -1,11 +1,11 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { Modal, Setting, SliderComponent, TFile } from "obsidian";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Modal, Setting, TFile } from "obsidian";
import { getEA } from "src";
import { DEVICE } from "src/Constants";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground } from "src/utils/Utils";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/Utils";
export class ExportDialog extends Modal {
private ea: ExcalidrawAutomate;
@@ -33,20 +33,33 @@ export class ExportDialog extends Modal {
private view: ExcalidrawView,
private file: TFile,
) {
super(app);
super(plugin.app);
this.ea = getEA(this.view);
this.api = this.ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
this.padding = getExportPadding(this.plugin,this.file);
this.scale = getPNGScale(this.plugin,this.file)
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
this.embedScene = false;
this.embedScene = shouldEmbedScene(this.plugin, this.file);
this.exportSelectedOnly = false;
this.saveToVault = true;
this.transparent = !getWithBackground(this.plugin, this.file);
this.saveSettings = false;
}
destroy() {
this.app = null;
this.plugin = null;
this.ea.destroy();
this.ea = null;
this.view = null;
this.file = null;
this.api = null;
this.theme = null;
this.selectedOnlySetting = null;
this.containerEl.remove();
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Export Image`);

View File

@@ -11,7 +11,6 @@ import {
SuggestModal,
Scope,
} from "obsidian";
import { t } from "../lang/helpers";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
class Suggester<T> {
@@ -132,7 +131,7 @@ class Suggester<T> {
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
items: T[] = [];
suggestions: HTMLDivElement[];
popper: PopperInstance;
popper: WeakRef<PopperInstance>;
//@ts-ignore
scope: Scope = new Scope(this.app.scope);
suggester: Suggester<FuzzyMatch<T>>;
@@ -197,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
this.app.keymap.pushScope(this.scope);
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
this.popper = createPopper(this.inputEl, this.suggestEl, {
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
{
@@ -213,7 +212,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
},
},
],
});
}));
}
onEscape(): void {
@@ -225,11 +224,15 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
this.app.keymap.popScope(this.scope);
this.suggester.setSuggestions([]);
if (this.popper) {
this.popper.destroy();
if (this.popper?.deref()) {
this.popper.deref().destroy();
}
this.suggestEl.detach();
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
this.inputEl.removeEventListener("blur", this.close.bind(this));
this.suggestEl.detach();
}
createPrompt(prompts: HTMLSpanElement[]) {
if (!this.promptEl) {

View File

@@ -0,0 +1,83 @@
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { t } from "src/lang/helpers";
export const showFrameSettings = (ea: ExcalidrawAutomate) => {
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
// Create modal dialog
const frameSettingsModal = new ea.obsidian.Modal(app);
frameSettingsModal.onOpen = () => {
const {contentEl} = frameSettingsModal;
contentEl.createEl("h1", {text: t("FRAME_SETTINGS_TITLE")});
const settings = { enabled, clip, name, outline };
// Add toggles
const enableFramesSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_ENABLE"))
.addToggle(toggle => toggle
.setValue(settings.enabled)
.onChange(value => {
settings.enabled = value;
hideComponent(displayFrameNameSetting, !value);
hideComponent(displayFrameOutlineSetting, !value);
hideComponent(enableFrameClippingSetting, !value);
})
);
const displayFrameNameSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTIGNS_NAME"))
.addToggle(toggle => toggle
.setValue(settings.name)
.onChange(value => settings.name = value)
);
const displayFrameOutlineSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_OUTLINE"))
.addToggle(toggle => toggle
.setValue(settings.outline)
.onChange(value => settings.outline = value)
);
const enableFrameClippingSetting = new ea.obsidian.Setting(contentEl)
.setName(t("FRAME_SETTINGS_CLIP"))
.addToggle(toggle => toggle
.setValue(settings.clip)
.onChange(value => settings.clip = value)
);
// Hide or show components based on initial state
hideComponent(displayFrameNameSetting, !settings.enabled);
hideComponent(displayFrameOutlineSetting, !settings.enabled);
hideComponent(enableFrameClippingSetting, !settings.enabled);
// Add OK button
new ea.obsidian.Setting(contentEl)
.addButton(button => button
.setButtonText("OK")
.onClick(() => {
// Update appState with new settings
ea.viewUpdateScene({
// @ts-ignore
appState: {
frameRendering: settings
},
storeAction: "update",
});
frameSettingsModal.close();
})
);
};
frameSettingsModal.onClose = () => {
ea.destroy();
}
frameSettingsModal.open();
};
// Function to hide or show a component
function hideComponent(comp:any, value:any) {
comp.settingEl.style.display = value ? "none" : "";
}

View File

@@ -1,14 +1,21 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -36,15 +43,23 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
return item.path;
}
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
async onChooseItem(item: TFile, _: KeyboardEvent): Promise<void> {
if(!item) return;
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
const svg = await app.vault.read(item);
const ea = getEA(this.view) as ExcalidrawAutomate;
const svg = await this.app.vault.read(item);
if(!svg || svg === "") return;
ea.importSVG(svg);
ea.addElementsToView(true, true, true,true);
ea.addToGroup(ea.getElements().map(el=>el.id));
await ea.addElementsToView(true, true, true,true);
ea.destroy();
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
public start(view: ExcalidrawView) {

View File

@@ -0,0 +1,53 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
private addText: Function;
destroy() {
this.app = null;
this.addText = null;
}
constructor(app: App) {
super(app);
this.app = app;
this.limit = 20;
this.setInstructions([
{
command: t("SELECT_COMMAND"),
purpose: "",
},
]);
this.setPlaceholder(t("SELECT_COMMAND_PLACEHOLDER"));
this.emptyStateText = t("NO_MATCHING_COMMAND");
}
getItems(): any[] {
//@ts-ignore
return this.app.commands.listCommands();
}
getItemText(item: any): string {
return item.name;
}
onChooseItem(item: any): void {
const cmdId = item?.id;
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
this.addText = null;
}
public start(addText: Function) {
this.addText = addText;
this.open();
}
onClose(): void {
window.setTimeout(()=>{
this.addText = null;
}) //onChooseItem must run first
super.onClose();
}
}

View File

@@ -1,16 +1,30 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { isALT, scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { fileURLToPath } from "url";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
this.inputEl.onkeyup = null;
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -56,12 +70,14 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
}
onChooseItem(item: TFile, event: KeyboardEvent): void {
const ea = this.plugin.ea.getAPI(this.view);
const ea = getEA(this.view);
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
const scaleToFullsize = scaleToFullsizeModifier(event);
(async () => {
//this.view.currentPosition = this.position;
await ea.addImage(0, 0, item, !scaleToFullsize);
ea.addElementsToView(true, true, true);
await ea.addElementsToView(true, true, true);
ea.destroy();
})();
}

View File

@@ -1,15 +1,22 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
private drawingPath: string;
constructor(app: App) {
super(app);
this.app = app;
destroy() {
this.app = null;
this.addText = null;
this.drawingPath = null;
}
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
this.app = plugin.app;
this.limit = 20;
this.setInstructions([
{
@@ -45,7 +52,15 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
true,
);
}
this.addText(`[[${filepath + (item.alias ? `|${item.alias}` : "")}]]`);
const link = getLink(this.plugin,{embed: false, path: filepath, alias: item.alias});
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
}
onClose(): void {
window.setTimeout(()=>{
this.addText = null
}); //make sure this happens after onChooseItem runs
super.onClose();
}
public start(drawingPath: string, addText: Function) {

View File

@@ -2,12 +2,18 @@ import { App, FuzzySuggestModal, TFile } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;
destroy() {
this.app = null;
this.plugin = null;
this.view = null;
}
constructor(plugin: ExcalidrawPlugin) {
super(plugin.app);
this.plugin = plugin;
@@ -34,12 +40,11 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
}
onChooseItem(item: TFile): void {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this.view);
const ea = getEA(this.view);
(async () => {
await ea.addImage(0, 0, item);
ea.addElementsToView(true, false, true);
await ea.addElementsToView(true, false, true);
ea.destroy();
})();
}
@@ -47,4 +52,12 @@ export class InsertMDDialog extends FuzzySuggestModal<TFile> {
this.view = view;
this.open();
}
onClose(): void {
//deley this.view destruction until onChooseItem is called
window.setTimeout(() => {
this.view = null;
});
super.onClose();
}
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent, TFile } from "obsidian";
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
@@ -6,12 +6,17 @@ import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { t } from "src/lang/helpers";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private frame: boolean = false;
private gapSize:number = 20;
private groupPages: boolean = false;
private direction: "down" | "right" = "right";
private numColumns: number = 1;
private numRows: number = 1;
private lockAfterImport: boolean = true;
private pagesToImport:number[] = [];
private pageDimensions: {width: number, height: number} = {width: 0, height: 0};
@@ -21,12 +26,11 @@ export class InsertPDFModal extends Modal {
private pdfFile: TFile;
private dirty: boolean = false;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
super(plugin.app);
}
open (file?: TFile) {
@@ -46,15 +50,24 @@ export class InsertPDFModal extends Modal {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfFrame = this.frame;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
this.plugin.settings.pdfNumRows = this.numRows;
this.plugin.settings.pdfDirection = this.direction;
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
this.plugin.saveSettings();
await this.plugin.saveSettings();
}
if(this.pdfDoc) {
this.pdfDoc.destroy();
this.pdfDoc = null;
}
this.plugin = null;
this.view = null;
this.app = null;
this.imageSizeMessage.remove();
this.setImageSizeMessage = null;
}
private async getPageDimensions (pdfDoc: any) {
@@ -110,8 +123,12 @@ export class InsertPDFModal extends Modal {
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.frame = this.plugin.settings.pdfFrame;
this.gapSize = this.plugin.settings.pdfGapSize;
this.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
this.numRows = this.plugin.settings.pdfNumRows;
this.direction = this.plugin.settings.pdfDirection;
this.lockAfterImport = this.plugin.settings.pdfLockAfterImport;
this.importScale = this.plugin.settings.pdfImportScale;
@@ -125,13 +142,13 @@ export class InsertPDFModal extends Modal {
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importMessage.innerText = t("IPM_SELECT_PDF");
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
@@ -148,12 +165,25 @@ export class InsertPDFModal extends Modal {
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
numPagesMessage.innerText = t("IPM_SELECT_PDF");
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
}
let pageRangesTextComponent: TextComponent
let importPagesMessage: HTMLParagraphElement;
const rangeOnChange = (value:string) => {
const pages = this.createPageListFromString(value);
if(pages.length > 15) {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
} else {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
}
importButtonMessages();
}
const setFile = async (file: TFile) => {
if(this.pdfDoc) await this.pdfDoc.destroy();
this.pdfDoc = null;
@@ -163,6 +193,8 @@ export class InsertPDFModal extends Modal {
this.pdfFile = file;
if(this.pdfDoc) {
numPages = this.pdfDoc.numPages;
pageRangesTextComponent.setValue(`1-${numPages}`);
rangeOnChange(`1-${numPages}`);
importButtonMessages();
numPagesMessages();
this.getPageDimensions(this.pdfDoc);
@@ -182,47 +214,116 @@ export class InsertPDFModal extends Modal {
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
let importPagesMessage: HTMLParagraphElement;
let pageRangesTextComponent: TextComponent
new Setting(ce)
.setName("Pages to import")
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
.setDesc("e.g.: 1,3-5,7,9-10")
.addText(text => {
pageRangesTextComponent = text;
text
.setPlaceholder("e.g.: 1,3-5,7,9-10")
.onChange((value) => {
const pages = this.createPageListFromString(value);
if(pages.length > 15) {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
} else {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
}
importButtonMessages();
})
.setValue("")
.onChange((value) => rangeOnChange(value))
text.inputEl.style.width = "100%";
})
importPagesMessage = ce.createEl("p", {text: ""});
let bbToggle: ToggleComponent;
let fToggle: ToggleComponent;
let laiToggle: ToggleComponent;
this.frame = this.borderBox ? false : this.frame;
new Setting(ce)
.setName("Add border box")
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
.addToggle(toggle => {
bbToggle = toggle;
toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
if(value) {
this.frame = false;
fToggle.setValue(false);
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_ADD_FRAME_NAME"))
.setDesc(t("IPM_ADD_FRAME_DESC"))
.addToggle(toggle => {
fToggle = toggle;
toggle
.setValue(this.frame)
.onChange((value) => {
this.frame = value;
if(value) {
this.borderBox = false;
bbToggle.setValue(false);
if(!this.lockAfterImport) {
this.lockAfterImport = true;
laiToggle.setValue(true);
}
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_GROUP_PAGES_NAME"))
.setDesc(t("IPM_GROUP_PAGES_DESC"))
.addToggle(toggle => toggle
.setValue(this.borderBox)
.setValue(this.groupPages)
.onChange((value) => {
this.borderBox = value;
this.groupPages = value
this.dirty = true;
}))
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
.addToggle(toggle => {
laiToggle = toggle;
toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
})
})
let numColumnsSetting: Setting;
let numRowsSetting: Setting;
const colRowVisibility = () => {
switch(this.direction) {
case "down":
numColumnsSetting.settingEl.style.display = "none";
numRowsSetting.settingEl.style.display = "";
break;
case "right":
numColumnsSetting.settingEl.style.display = "";
numRowsSetting.settingEl.style.display = "none";
break;
}
}
new Setting(ce)
.setName("Import direction")
.addDropdown(dropdown => dropdown
.addOptions({
"down": "Top > Down",
"right": "Left > Right"
})
.setValue(this.direction)
.onChange(value => {
this.direction = value as "down" | "right";
colRowVisibility();
this.dirty = true;
}))
let columnsText: HTMLDivElement;
new Setting(ce)
numColumnsSetting = new Setting(ce);
numColumnsSetting
.setName("Number of columns")
.addSlider(slider => slider
.setLimits(1, 100, 1)
@@ -239,6 +340,26 @@ export class InsertPDFModal extends Modal {
el.innerText = ` ${this.numColumns.toString()}`;
});
let rowsText: HTMLDivElement;
numRowsSetting = new Setting(ce);
numRowsSetting
.setName("Number of rows")
.addSlider(slider => slider
.setLimits(1, 100, 1)
.setValue(this.numRows)
.onChange(value => {
this.numRows = value;
rowsText.innerText = ` ${value.toString()}`;
this.dirty = true;
}))
.settingEl.createDiv("", (el) => {
rowsText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.numRows.toString()}`;
});
colRowVisibility();
let gapSizeText: HTMLDivElement;
new Setting(ce)
.setName("Size of gap between pages")
@@ -256,7 +377,7 @@ export class InsertPDFModal extends Modal {
el.style.textAlign = "right";
el.innerText = ` ${this.gapSize.toString()}`;
});
const importSizeSetting = new Setting(ce)
.setName("Imported page size")
.setDesc(`${this.pageDimensions.width*this.importScale} x ${this.pageDimensions.height*this.importScale}`)
@@ -303,6 +424,7 @@ export class InsertPDFModal extends Modal {
topX,
topY,
this.pdfFile.path + `#page=${page}`,
false,
false);
const imgEl = ea.getElement(imageID) as any;
imgEl.width = imgWidth;
@@ -311,8 +433,28 @@ export class InsertPDFModal extends Modal {
ea.addToGroup([boxID,imageID]);
column = (column + 1) % this.numColumns;
if(column === 0) row++;
if(this.frame) {
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
ea.addElementsToFrame(frameID, [boxID,imageID]);
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
}
switch(this.direction) {
case "right":
column = (column + 1) % this.numColumns;
if(column === 0) row++;
break;
case "down":
row = (row + 1) % this.numRows;
if(row === 0) column++;
break;
}
}
if(this.groupPages) {
const ids = ea.getElements()
.filter(el=>!this.frame || (el.type === "frame"))
.map(el => el.id);
ea.addToGroup(ids);
}
await ea.addElementsToView(true,true,false);
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
@@ -320,6 +462,7 @@ export class InsertPDFModal extends Modal {
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
api.selectElements(viewElements);
api.zoomToFit(viewElements);
ea.destroy();
this.close();
})
importButton = button;

View File

@@ -6,7 +6,7 @@ If you'd like to learn more, please subscribe to my YouTube channel: [Visual PKM
Thank you & Enjoy!
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/o0exK-xFP3k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
@@ -17,6 +17,977 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.3.0": `
I am moving to a new release approach aiming to publish one update per month to the Obsidian script store. If you want to continue to receive more frequent updates with new features and minor bug fixes, then join the beta testing team. [#1912](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1912)
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/2poSS-Z91lY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Elbow connectors: https://x.com/excalidraw/status/1819084086222393554
## Fixed
- Convert Markdown to Excalidraw did not work correctly when there was ${String.fromCharCode(96)}---${String.fromCharCode(96)} anywhere in the file, but no frontmatter (e.g. a table)
- Fixed Obsidian move tab to new window
- Fixed duplicating bound arrows without its bound elements throwing error [#8315](https://github.com/excalidraw/excalidraw/issues/8315)
`,
"2.2.13": `
## Fixed
- Could not undo element after pasting [#1906](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1906)
- Links broke after renaming an Excalidraw file using the F2 shortcut [#1907](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1907)
- Unable to open or convert very large ${String.fromCharCode(96)}.excalidraw${String.fromCharCode(96)} file, e.g. BoaPs you can download from [here](https://ko-fi.com/zsolt/shop)
`,
"2.2.12": `
## Fixed
- Rename moved files to root folder [#1905](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1905)
- Fonts not displaying correctly in cached image previews
`,
"2.2.11": `
<img alt="badges" src="https://github.com/user-attachments/assets/7591b523-6bc6-46ff-b552-5c3492139e4c" referrerpolicy="no-referrer" style="width: 100%;">
## New
- Font picker with additional fonts (not yet fully configurable, but that will come in due time) [#8012](https://github.com/excalidraw/excalidraw/pull/8012)
- Introducing Visual Thinking Badges. The more you use Excalidraw the higher your rank will be. Levels are: Bronze, Silver, Gold and Platinum.
## Fixed
- Embedded PDF was not visible on phones [#1904](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1904)
- F2 does not rename files in Excalidraw View [#1900](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1900)
- Wireframe to Code now honors the GPT model settings in plugin settings. [#1901](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1901)
- Updated ExcaliAI to support gpt-4o for vision. [#1859](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1859) 🙏@Saik0s
- Minor fixes from excalidraw.com [#8287](https://github.com/excalidraw/excalidraw/pull/8287), [#8285](https://github.com/excalidraw/excalidraw/pull/8285)
`,
"2.2.10": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/sjZfdqpxqsg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Drastically degraded rendering performance when zoomed in and when arrows with labels are used. [#8267](https://github.com/excalidraw/excalidraw/pull/8267), [#8266](https://github.com/excalidraw/excalidraw/pull/8266), [#1893](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1893)
- Frame title font in exports.
## New
- Area, Group, Frame, and Clipped-Frame references to images now also work when pasting images to Excalidraw.
- The new reference type ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} works in the same way as ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} but will display the elements clipped by the frame. ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} will always display the image with zero padding.
- New command palette action: ${String.fromCharCode(96)}Frame Settings${String.fromCharCode(96)} gives you fine-grained control over how frames are rendered. Frame settings will also be reflected in image exports. For example, if you hide the frame name or outline, then in exports they will not be visible.
`,
"2.2.9": `
## New
- Improved the "Open the back-of-the-note of the selected Excalidraw image" action. It now works with grouped elements and keeps the popout window within the visible screen area when elements are close to the top of the canvas. Note: Due to an Obsidian bug, I do not recommend using this feature with versions 1.6.0 - 1.6.6, if you have Obsidian Sync enabled, because Obsidian may freeze when closing the popout window. It functions properly in Obsidian versions before 1.6.0 and from 1.6.7 onwards.
## Fixed
- Drag and drop from a local folder (outside Obsidian) resulted in duplicate images.
- Insert Link Action did not work [#1873](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1873)
- Insert Obsidian Command Action did not work
- Element link for text element got deleted when editing the text. [#1878](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1878)
- When back-of-the-drawing Section Headings have spaces in them, clicking the link opens the drawing side not the markdown side. [#1877](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1877)
- obsidian:// links did not work as expected. [#1872](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1872)
- copying and moving a rectangle with text, moves the text unexpectedly. The issue should now be resolved (at least much less likely to occur) [#1867](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1867)
`,
"2.2.8": `
While this release may appear modest with no new features, it represents nearly 50 hours of dedicated work. Here's what's been improved:
- **Enhanced Memory Management**: Significant improvements to optimize memory usage.
- Bug Fixes:
- Support for multi-file drag and drop from the operating system.
- Correct theming of animated GIFs as Embeddables.
- Several other minor bug fixes.
Please note that due to extensive refactoring of the codebase, there may be some unexpected issues. Thanks for your understanding and patience.
`,
"2.2.7": `
## New
- In Miscellaneous Settings: added **Load Excalidraw Properties into Obsidian Suggester**. This setting toggles the automatic loading of Excalidraw properties at startup. Enabled by default for easy use of front matter properties. Disabling it prevents auto-loading, but you'll need to manually remove unwanted properties using Obsidian properties view. A plugin restart is required after enabling auto-loading.
## Fixed
- Zotero support [1835](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1835)
- Lines binding to elements and selections [#8146](https://github.com/excalidraw/excalidraw/issues/8146), and plugin getting stuck with dragging an element [#8131](https://github.com/excalidraw/excalidraw/issues/8131)`
,
"2.2.6": `
## Fixed
- CTRL+F search for text elements in drawing, the result did not get selected. This was a regression in 2.2.5 [#1822](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1822)
## New
- Zotero compatibility support for back-of-the-side markdown notes. This needs to be enabled in plugin settings under Compatibility [#1820](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1820)
## New from Excalidraw.com
- ${String.fromCharCode(96)}Stats & Element Properties${String.fromCharCode(96)}, accessible via the context menu, is now editable, e.g. you can type in the exact position and size of objects, change font size and set element angle.
- Pasting mermaid diagrams from chatGPT will embed a diagram instead of the text
`,
"2.2.5": `
## Fixed
- Cursor visibility in dark mode [#1812](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1812)
- SVG to Excalidraw now...
- converts elements inside the ${String.fromCharCode(96)}<switch>${String.fromCharCode(96)} tag, improving compatibility with SVGs from [The Noun Project](https://thenounproject.com/)
- sets visibility for all elements, preventing invisible converted images.
- Cached images sometimes lost their font face and natural size when nested in an Excalidraw scene. This issue occurred when drawings were embedded in a markdown note (native SVG) and nested in a drawing simultaneously. Depending on the update and render sequence, these drawings sometimes appeared incorrectly in the Excalidraw scene.
`,
"2.2.4":`
<div style="text-align: center;">
<a data-tooltip-position="top" aria-label="https://youtube.com/shorts/zF1p2yfk4f4" rel="noopener" class="external-link" href="https://youtube.com/shorts/zF1p2yfk4f4" target="_blank">
<img src="https://private-user-images.githubusercontent.com/14358394/335857018-c4f5c4c7-9b8f-427f-aa6f-8c1b189610af.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTczMjQxNDksIm5iZiI6MTcxNzMyMzg0OSwicGF0aCI6Ii8xNDM1ODM5NC8zMzU4NTcwMTgtYzRmNWM0YzctOWI4Zi00MjdmLWFhNmYtOGMxYjE4OTYxMGFmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MDIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjAyVDEwMjQwOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdhZGUwNDRkZmM2NmJjNTNiYjUwNjMxMmU2MGEyZTQwZGQwNmUyZmI5ZDFhNzMwMzg2OThjMjhmZmJkNzNiZDkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.wZfkXmBRcXwz0pN6q0EEvmwtxVAB9ymPk9a9upmGXYE" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
</a>
</div>
## New from Excalidraw.com
- You can now set the text width even during creation. Simply drag with the text tool. Note, there's a minimum distance before the text enters the wrapped mode so there aren't false positives. [See example here](https://x.com/excalidraw/status/1795468201335075000)
## New
- Updated zh-cn translation. Thank you @dmscode
- New context menu and command palette action: "Move back-of-note card to File". This is only active when an eligible embeddable element is selected.
## Fixed
- Setting different autosaveIntervals on Desktop and Mobile will no longer trigger unnecessary commits to settings each time you open Excalidraw on a different device. Thanks @jmhammond for the contribution! [#1805](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1805), [#1652](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1652), [#888](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/888)
## Fixed in ExcalidrawAutomate
- ${String.fromCharCode(96)}getCM(color)${String.fromCharCode(96)} was missing from ${String.fromCharCode(96)}ea.help()${String.fromCharCode(96)}. It is now added. getCM returns a ColorMaster object. ColorMaster is a powerful library should you want to create scripts to manipulate colors. Check out my [Scripting Colors](https://youtu.be/7gJDwNgQ6NU) video should you want to learn more. [#1806](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1806)
`,
"2.2.3":`
## Fixed
- Undo history was not properly initialized [#1791](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1785)
- Excalidraw did not save edits when switching to markdown view mode with a hotkey or terminating the popout window
- SVG export did not maintain the aspect ratio of manually distorted images [#1780](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1780)
## New
- In pen mode, double tapping the screen will toggle the eraser tool when using freedraw tool, or one of the other tools in locked mode.
- New setting under "Excalidraw appearance and behavior" to disable rendering of Excalidraw drawings in hover previews, in case the file has the ${String.fromCharCode(96)}excalidraw-open-md: true${String.fromCharCode(96)} frontmatter property [#1795](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1795)
- Additional foolproofing of ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. The file is now more resilient to automated linting and other changes. There is also a new setting under "Compatibility Features" to add a dummy first text element to ${String.fromCharCode(96)}## Text Elements${String.fromCharCode(96)}. You can use this feature if your auto-linter adds empty lines after section headings.
- Pasting markdown code blocks will create a back-of-the-note card with the code block. CTRL+SHIFT+V will paste the text as a normal text element. When copying code from Chat GPT the markdown code fence (triple backtick) is missing. In this case, you may use the new context menu action "Paste code block" to create a back of the note card with the code block.
- Pasting long text will be wrapped in the text element.
## New in ExcalidrawAutomate
- Updated viewUpdateScene: This now implements the [new Excalidraw API](https://github.com/excalidraw/excalidraw/pull/7898)
${String.fromCharCode(96, 96, 96)}ts
viewUpdateScene (
scene: {
elements?: ExcalidrawElement[],
appState?: AppState,
files?: BinaryFileData,
commitToHistory?: boolean,
storeAction?: "capture" | "none" | "update",
},
restore: boolean = false,
):void ;
${String.fromCharCode(96, 96, 96)}
- Updated addText. The function now supports the new text-wrapping feature
${String.fromCharCode(96, 96, 96)}ts
addText(
topX: number,
topY: number,
text: string,
formatting?: {
autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the container. If set to false, you must set a width value as well.
wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
},
id?: string,
): string
${String.fromCharCode(96, 96, 96)}
`,
"2.2.2":`
## Fixed
- ExcaliBrain stopped working with 2.2.0
![I apologize](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/3b05aa28-788d-4329-9721-798ad58a6ca2)
`,
"2.2.1":`
## Fixed
- Text height becomes unreadable after 2.2.0 update [#1784](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1784)
- Images are loaded with a rounded border when loading old Excalidraw files
- Embedded Excalidraw images cache gets stuck with old version of the image
- Extremely long loading times with legacy (3+ years old) Excalidraw files
`,
"2.2.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/dV0NEOwn5NM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
⚠️⚠️⚠️ BREAKING CHANGE ⚠️⚠️⚠️
Files you save with 2.2.0 are not backward compatible with earlier plugin versions!
## New from excalidraw.com
- Wrapable text elements (without the need for transparent sticky notes!)
## New
- File format. I nested all Excalidraw markup under ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)}. Here's the new structure.
${String.fromCharCode(96,96,96)}markdown
---
excalidraw-plugin: parsed
other-frontmatter-properties: values
---
back of the note bla bla bla
# Excalidraw Data
## Text Element
## Element Links
## Embedded Files
%%
## Drawing
%%
${String.fromCharCode(96,96,96)}
- When opening Excalidraw in Markdown ${String.fromCharCode(96)}# Excalidraw Data${String.fromCharCode(96)} will be folded.
- New command palette action: ${String.fromCharCode(96)}Open the back-of-the-note of the selected Excalidraw image${String.fromCharCode(96)}. The action is only visible when selecting an embedded Excalidraw drawing in the Scene. On a desktop, the command will open the back of the selected card in a popout window, and on a mobile, in a new tab.
## Fixed
- Drag and drop from Finder/Explorer (OS external). Images will retain their filenames. PDFs will be imported to the Vault. [#1779](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1779)
`,
"2.1.8":`
## Fixed
- Fixing issues that surfaced after upgrading to Obsidian 1.6.0
- Fixed Excalidraw hover previews
- Cursor for editing links, text elements, and frame names became virtually invisible if Obsidian was in dark mode and Excalidraw in light mode and vica versa.
- Rendering Excalidraw drawings in Markdown views, right after Obsidian loaded did not work.
- I implemented more graceful fail if you submitted a malformed SVG for SVG to Excalidraw conversation. [#1768](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1768)
## New
- New setting under "Save" in plugin settings to NOT decompress JSON when switching to Markdown view mode. For details see description under "Save" settings. The benefit is smaller file size and fewer results in the Obsidian search. If you want to edit the JSON, you can always manually decompress JSON in markdown view mode with the command palette action "Excalidraw: Decompress JSON".
`,
"2.1.7:":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Updates from Excalidraw.com
- Improved undo management.
- Improved handle to scale images from the side.
- Changed arrow binding behavior.
- Many other minor fixes and improvements.
## New
- Introduced image caching for nested (embedded) Excalidraw drawings on the scene. This enhancement should lead to improved scene loading times, especially when dealing with numerous embedded Excalidraw drawings.
- Added new OCR Command Palette actions. Users can now re-run OCR and run OCR for selected elements.
## Fixed
- Fixed an issue where cropping an embeddable PDF frame in the Excalidraw Scene caused distortion based on the embeddable element's aspect ratio. ([#1756](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1756))
- Removed the listing of ${String.fromCharCode(96)}# Embedded files${String.fromCharCode(96)} section when adding a "Back of the note card". ([#1754](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1754))
- Resolved the issue where closing the on-screen keyboard with the keyboard hide button of your phone, instead of tapping somewhere else on the Excalidraw scene, did not resize the scene correctly. ([#1729](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729))
- Fixed the problem where pasting a text element as text into markdown incorrectly pasted the text to the end of the MD note, with line breaks as rendered on screen in Excalidraw. Also addressed the issue where pasting an image element as an image resulted in it being pasted to the end of the document. ([#1749](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1749))
- Corrected the color inversion of embedded images when changing the theme from light to dark, then back from dark to light, and again from light to dark on the third change.
- Addressed the problem where cropping an image while unlocking and rotating it in the cropper did not reflect the rotation. Note that rotating the image in Cropper required switching to markdown view mode, changing the "locked": true property to false, then switching back to Excalidraw mode. This issue likely impacted only a very few power users. ([#1745](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1745))
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[];
${String.fromCharCode(96,96,96)}
`,
"2.1.6":`
## Two minor fixes
- Scaling of LaTeX formulas when the formula is changed
- If the back of the note card only contains a block embed ${String.fromCharCode(96)}![[embed]]${String.fromCharCode(96)} this got removed when saving the Excalidraw file. This issue has been present since November, 2021 (v1.4.9).
`,
"2.1.5":`
## New
- Save "Snap to objects" with the scene state. If this is the only change you make to the scene, force save it using CTRL+S (note, use CTRL on Mac as well).
- Added "Copy markdown link" to the context menu.
## Fixed
- Paste operation occasionally duplicated text elements.
- Pasting multiple instances of the same image from excalidraw.com or another instance of Obsidian, or pasting an image from anywhere and making copies with ALT/OPT + drag immediately after pasting (before autosave triggered) led to broken images when reopening the drawing.
- CTRL/CMD+Click on a Text Element with an element link did not work (previously, you had to click the top right link indicator). Now, you can click anywhere on the element.
- Hover preview for elements with a link only worked when hovering over the element link. Now, you can hover anywhere. If there are multiple elements with links, the top-level element will take precedence.
- Link navigation within drawing when the "Focus on Existing Tab" feature is enabled under "Links, transclusion and TODOs" in settings works again.
- If a link points to a back-of-the-card section or block the drawing will automatically switch to markdown view mode and navigate to the block or section.
- DynamicSytle, dark mode when canvas background is set to transparent.
- Scale to maintain the aspect ratio of a markdown notes embedded as images.
- You can now borrow interactive markdown embeds to tables, blockquotes, list elements and callouts - not just paragraphs.
- Back of the drawing cards:
- Leaving the Section Name empty when creating the first back of the card note resulted in an error.
- If you add the markdown comment (${String.fromCharCode(96)}%%${String.fromCharCode(96)}) directly before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, a trailing ${String.fromCharCode(96)}#${String.fromCharCode(96)} will be added to your document, when adding a back of the card note. This is to hide the markdown comment from the card. The trailing (empty) ${String.fromCharCode(96)}#${String.fromCharCode(96)} will not be visible in reading mode, pdf exports, and when publishing with Obsidian Publish.
Here's a sample markdown structure of your document:
${String.fromCharCode(96,96,96)}markdown
---
excalidraw-plugin: parsed
---
# Your back of the card section
bla bla bla
#
%%
# Text Elements
... the rest of the Excalidraw file
${String.fromCharCode(96,96,96)}
`,
"2.1.4":`
## Fixed
- Fixed the **aspect ratio** of an Excalidraw embedded within another Excalidraw **not updating**. [#1707](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1707)
- Some plugins automatically add document properties to all files in the Vault. Users with this configuration were **unable to run Excalidraw scripts**. Excalidraw now removes document properties from the script before execution.
- The very last markdown edit sometimes **wasn't saved when immediately switching from Markdown to Excalidraw View**. I now force a save before switching views.
- The setting to disable/enable ${String.fromCharCode(96)}CTRL/CMD + CLICK on text with [[links]] or [](links) to open them${String.fromCharCode(96)} works again. [#1704](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1704)
- **Annotation and cropping** of images in Markdown notes now also work **with Markdown links that have encoded characters** e.g.: ${String.fromCharCode(96)}![images with](markdown%20links)${String.fromCharCode(96)}.
- Solved compatibility issue of **Taskbone OCR on Android**.
## New
- New settings:
- Under "Appearance and Behavior": Option to **render Excalidraw file as an image in Markdown reading mode**. This setting is disabled by default. [#1706](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1706), [#1705](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1705)
- Under "Embedding Excalidraw ... and Exporting"/"Export Settings": Option to **render Excalidraw file as an image when exporting to PDF** in Markdown mode. This option is disabled by default. When enabled, exporting an Excalidraw drawing in markdown view mode to PDF will render the image on the page.
- **Enhanced annotation and cropping** of images in Markdown documents:
- Newly embedded **links will now follow the style of the original link**. If the original format was a ${String.fromCharCode(96)}![markdown](link)${String.fromCharCode(96)}, the annotated file will follow this format. For ${String.fromCharCode(96)}[[wiki links]]${String.fromCharCode(96)}, it will follow that style. Additionally, if an alias was specified like ${String.fromCharCode(96)}[[link|alias]]${String.fromCharCode(96)}, the annotated or cropped image will retain the alias.
- Introduced a new setting under "Saving" titled **"Preserve image size when annotating"**. This setting is disabled by default. When enabled, the embed link replacing the annotated image will maintain the size of the original image.
- Option to **automatically embed the scene in exported PNG and SVG image files**. Including the scene will allow users to open the picture on Excalidraw.com or in another Obsidian Vault as an editable Excalidraw file.New setting is under the Export category. The new frontmatter tag is: ${String.fromCharCode(96)}excalidraw-export-embed-scene: true/false${String.fromCharCode(96)}.
`,
"2.1.3":`
This is a republish of 2.1.2 with a minor change. Sorry about the frequent releases. I will hold back for a few weeks now.
`,
"2.1.2":`
## Quality of Life Improvements
- The "Insert Any File" option that disappeared from the Command Palette is now restored. [#1690](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1690)
- Improved two-finger pan speed.
- Fixed text wrapping issue that caused text to jump around when editing text in a sticky note when the Obsidian zoom level was not set to 100%.
- Mask Generation in [ExcaliAI](https://youtu.be/3G8hsV-V-gQ) Edit Image now works properly again. [#1684](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1684)
- Fixed aspect ratio change for .jpg, .png, .bmp, .webp, .SVG (non-Excalidraw) images. Previously, if the image was distorted (i.e. you held SHIFT while resizing it), it would revert to the original aspect ratio upon saving the drawing. Resetting the aspect ratio is the desired behavior for nested Excalidraw drawings since you might have changed the source image and want it to still display with the correct aspect ratio, however for other image files, the behavior is not desired. [#1698](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698)
- The command palette action "Set selected image element size to 100% of original" now works even on freshly pasted images, not just after saving the drawing. ([#1695](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1698))
- If a text element has an element link (CTRL/CMD+K), but the link was not reflected in the Element Text, then CTRL/CMD+clicking the text element did not navigate to the link, only clicking the link indicator did. Now you can also CTRL/CMD click anywhere on the text element and it will navigate. Note, however, that links in the text element text take precedence over element links.
`,
"2.1.1":`
## Fixed
- Printing a markdown page that has an Excalidraw drawing on the back side, resulted in an empty PDF. This is now resolved.
## New
- Reduce the visual clutter by fading out the Excalidraw markup in markdown view mode. This feature needs to be enabled in plugin settings. You'll find the setting under ${String.fromCharCode(96)}Miscellaneous features${String.fromCharCode(96)}. Look for ${String.fromCharCode(96)}Fade out Excalidraw markup${String.fromCharCode(96)}. Depending on the location of the markdown comment ${String.fromCharCode(96)}%%${String.fromCharCode(96)}, if the comment starts before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} then the fading will start from ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, if the comment is before ${String.fromCharCode(96)}# Drawing${String.fromCharCode(96)} then the fading will only start with "drawing". If you delete the opening ${String.fromCharCode(96)}%%${String.fromCharCode(96)} the markup will be visible. Note, that if you place the comment before ${String.fromCharCode(96)}#Text Elements${String.fromCharCode(96)}, you will not be able to reference blocks in the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section, because Obsidian does not index blocks within comment blocks. Image references are not effective, they will work.
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/bb96cdb4-8c5f-4dc5-ad39-7fccee6d5cac" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
<img src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e627fdb7-6820-4d7d-97f9-a030016be9aa" referrerpolicy="no-referrer" style="width: 100%; margin: 0 auto;">
`,
"2.1.0":`
Bumping the version to 2.1.0 due to minor file format changes that aren't backward compatible. Essentially, 2.0.26 is already not backward compatible, but I forgot to update the version number.
If you haven't watched the [walkthrough video](https://youtu.be/tHUcD4rWIuY) for 2.0.26, I recommend you do so.
## New
- Settings under ${String.fromCharCode(96)}Excalidraw Appearance and Behavior${String.fromCharCode(96)}
- Configure visibility of the crosshair cursor when using the pen tool. [#1673](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1673)
- Set the time delay for long press to open drawings from markdown under "Link Click and Modifier Keys".
##
`,
"2.0.26":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/tHUcD4rWIuY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Minor updates from [Excalidraw.com](https://excalidraw.com). The key change is text measurements that should result in consistent text sizing between desktop and mobile devices.
- Now you can embed the markdown section of an Excalidraw note to your drawing. Simply select ${String.fromCharCode(96)}Insert ANY file${String.fromCharCode(96)}, choose the drawing, and select the relevant heading section to embed.
- This also works with "back-of-the-drawing" markdown sections. Use the context menu ${String.fromCharCode(96)}Add back-of-note Card${String.fromCharCode(96)}. The action is also available on the Command Palette and in the Excalidraw-Obsidian Tools Panel.
- Editing an embedded markdown note is now easier. Just press Enter when the element is selected. [#1650](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1650)
- The crosshair cursor is now hidden when the freedraw tool is active and using a pen. [#1659](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1659)
- ${String.fromCharCode(96)}Convert markdown note to Excalidraw Drawing${String.fromCharCode(96)} now converts an existing markdown note (not just empty notes) into a drawing. The original markdown note will be on the "back side of the drawing".
- Introducing ${String.fromCharCode(96)}Annotate image in Excalidraw${String.fromCharCode(96)}, which works very similarly to ${String.fromCharCode(96)}Crop and mask image${String.fromCharCode(96)}. You can replace an image in a markdown note or on the Obsidian Canvas with an Excalidraw drawing containing that image. You will be able to annotate the image in Excalidraw.
- Now you can reference frames in images embedded in markdown and canvas with frame names e.g. ${String.fromCharCode(96)}![[drawing#^frame=Frame 01]]${String.fromCharCode(96)}
- Excalidraw file format change:
- New frontmatter switch ${String.fromCharCode(96)}excalidraw-open-md${String.fromCharCode(96)}: If set to true, the file by default will open as a markdown file. You can switch to Excalidraw View Mode via the command palette action or by right-clicking the tab.
- Easter Egg: If you add a comment in front of ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then the entire Excalidraw data: markdown and JSON will be commented out, thus invisible when exporting to the web. If you remove the comment from before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then only the JSON will be commented out.
Before:
${String.fromCharCode(96,96,96)}markdown
#1657
%%
# Text Elements
...
# Drawing
${String.fromCharCode(96,96,96)}
After:
${String.fromCharCode(96,96,96)}markdown
# Text Elements
....
%%
# Drawing
${String.fromCharCode(96,96,96)}
`,
"2.0.25":`
# New - a small change that opens big opportunities
- You can now set a folder as the Excalidraw Template in settings (See under Basic). If a folder is provided, Excalidraw will treat drawings in that folder as templates and will prompt you to select the template to use for new drawings.
- I updated the <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md">Deconstruct Selected Elements into new Drawing</a> script to accommodate the new template setting.
`,
"2.0.24":`
Quality of Life Fixes!
## Fixed
- Text editing issue on mobile devices with an on-screen keyboard is now fixed 🥳. Previously, Excalidraw's UI fell apart when the keyboard was activated, and often even after you stopped editing, the canvas positioning was off. I hope to have solved the issue (we'll see after your testing and feedback!). This is one of those cases that seems insignificant but took enormous effort. It took me 2.5 full days of net time to figure out the root cause and the solution (this is not an exaggeration).
- Tool buttons did not get selected on the first click.
- Images flicker on Forced Save.
- Hover preview fixes:
- ${String.fromCharCode(96)}area=${String.fromCharCode(96)}, ${String.fromCharCode(96)}group=${String.fromCharCode(96)}, ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} references now display the part of the image as expected in hover preview (showed an empty preview until now).
- Block and section references to notes on the "back side of the drawing" now correctly show up in hover preview (showed an empty preview until now).
## New
- Default height setting in Plugin Settings. Thanks @leoccyao! [#1612](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1612)
`,
"2.0.23":`
## New
- Additional arrowheads (Circle, Circle Outline, Diamond, Diamond Outline, Triangle Outline) are now available via element properties.
- Setting under "Links and Transclusions" to show/hide second-order links
## Fixed
- some styling issues with dynamic styles (e.g.: text color of context menu)
## New in ExcalidrawAutomate
- Excalidraw Publish Support: New hook to modify the link in the exported SVGs. This is useful when you want to export SVGs to your website. If set, this callback is triggered whenever a drawing is exported to SVG. The string returned by the hook will replace the link in the exported SVG. The hook is only executed if the link is to a file internal to Obsidian. [1605](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605)
${String.fromCharCode(96,96,96)}js
onUpdateElementLinkForExportHook: (data: {
* originalLink: string,
* obsidianLink: string,
* linkedFile: TFile | null,
* hostFile: TFile,
* }) => string = null;
${String.fromCharCode(96,96,96)}
`,
"2.0.22":`
## Fixed
- BUG: Unable to load obsidian excalidraw plugin on ipad 15.x or older [#1525](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1525)
- BUG: ea.help does not display help if only function signature is available [#1601](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1601)
`,
"2.0.21":`
## New/changed
**"Focus on Existing Tab"**
- New Setting: Disabled by default.
- Prevents multiple instances of the same drawing from opening when clicking on links within Excalidraw.
- Overrides the "Reuse Adjacent Pane" option when the file is already open.
- Accessible under "Links, Transclusions, and TODOs" in plugin settings.
**Enhanced Context Menu Functions for Text Containers**
- Two new context menu functions added for containers with a text element:
- Right-click to select the text element only, allowing independent color changes from the container.
- Remove orphaned element links when the text element has a link but no longer includes a link in the text.
**Improved Laser Pointer Activation**
- Laser pointer activation on double tap in view mode removed due to interference with link navigation and other features.
- When the drawing is in "view" mode, laser pointer activation now available via long-press/right-click context menu.
- Alternatively, activate the laser pointer with "k" if you have a keyboard.
## Fixed
- **Older iOS and Android webview support**: Rebuilt all packages and dependencies with Node 18, hoping to address (sorry I can't reproduce/test these issues myself) compatibility issues with older iPad OS versions, up to 15.7. [#1525](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1525), and Android [1598](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1598)
- **Double-click navigation**: Fixed the issue where double-clicking an embedded image did not navigate to the link in view mode.
- **ExcaliBrain new file creation**: Resolved the issue with new file creation from ExcaliBrain. [#201](https://github.com/zsviczian/excalibrain/issues/201)
- **Canvas immersive style**: Removed Canvas immersive embedding style support from the Excalidraw stylesheet to address performance issues experienced by some users with various Obsidian themes. If you require this feature, you can add a CSS snippet with the provided code.
${String.fromCharCode(96,96,96)}css
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
::-webkit-scrollbar,
::-webkit-scrollbar-horizontal {
display: none;
}
background-color: transparent !important;
}
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
border: unset;
box-shadow: unset;
}
${String.fromCharCode(96,96,96)}
`,
"2.0.20":`
## Fixed in ExcalidrawAutomate
- Regression: ${String.fromCharCode(96)}ea.getMaximumGroups(elements)${String.fromCharCode(96)} stopped working after the 2.0.19 update. [#1576](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1576)
`,
"2.0.19":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/4wp6vLiIdGM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- When updating Excalidraw, some open drawings weren't automatically reopening. I hope I got this fixed (note this change will only have an effect when you receive the update after this).
- In dark mode, the frame header is challenging to see when modified [#1568](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1568).
## New
- Crop PDF pages:
- Available in Excalidraw, Markdown Notes, and on the Canvas.
- Crop the active page from the embedded PDF viewer and insert the cropped image into the current view, both in Excalidraw and on Canvas.
- New Command Palette Action: "Insert active PDF page as image." This action is functional in Excalidraw. If an embedded Obsidian-PDF-viewer is present, executing this command will insert the active page as an image into the Excalidraw scene.
- Two new settings introduced:
- "Basic" section allows setting the folder for crop files.
- "Saving/filename" section enables setting the prefix for crop files.
- PDF import now defaults to importing all pages.
- Rounded corners now available for images.
- Second-order links now encompass forward links from embedded Excalidraw Files.
- Clicking a cropped file in a markdown note or on Canvas will prompt to open the original file, not just the cropper.
`,
"2.0.18":`
## New
<div style="text-align: center;">
<a data-tooltip-position="top" aria-label="https://youtube.com/shorts/ST6h4uaXmnY" rel="noopener" class="external-link" href="https://youtube.com/shorts/ST6h4uaXmnY" target="_blank">
<img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/vintage-mask.png" referrerpolicy="no-referrer" style="width: 150px; margin: 0 auto;">
</a>
</div>
- [Crop Vintage Mask Script](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Crop%20Vintage%20Mask.md) adds rounded corner mask to cropped images. Install it from the script library.
- Advanced Setting: Modify image zoom memory limit for sharper zoom. See under "Non-Excalidraw.com Supported Features" in settings.
- Laser Pointer will not activate on double-click in ExcaliBrain
## Fixed
- Resolved cropping issue with rotated images.
## New in ExcalidrawAutomate
- You can now specify elementId to add functions: addLine, addArrow, addRect, etc.
- ea.help() now provides help on Script Engine utils functions as well
- ea.isExcalidrawMask(file?:TFile) will return true if the currently open view or the supplied file is an Excalidraw Mask file.
`,
"2.0.17":`
## Fixed
- Image cropping now supports dark mode
- Image cropping/carve out was not working reliably in some cases [#1546](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1546)
- Masking a mirrored image resulted in an off-positioned mask
## New
- Context menu action to convert SVG to Excalidraw strokes
- Updated Chinese translation (Thank you @tswwe)
`,
"2.0.16":`
## Fixed
- Image cropping did not work consistently with large image files on lower-powered devices [#1538](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1538).
- Mermaid editor was not working when Excalidraw was open in an Obsidian popout window [#1503](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1503)
`,
"2.0.15":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/uHFd0XoHRxE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Crop and Mask Images in Excalidraw, Canvas, and Markdown. (Inspired by @bonecast [#4566](https://github.com/excalidraw/excalidraw/issues/4566))
- Draw metadata around images but hide it on the export.
## Fixed
- Freedraw closed circles (2nd attempt)
- Interactive Markdown embeddable border-color (setting did not have an effect)
`,
"2.0.14":`
## New
- Stylus button now activates the eraser function. Note: This feature is supported for styluses that comply with industry-standard button events. Unfortunately, Samsung SPEN and Apple Pencil do not support this functionality.
## Fixed
- Improved handwriting quality. I have resolved the long-standing issue of closing the loop when ends of the line are close, making an "u" into an "o" ([#1529](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1529) and [#6303](https://github.com/excalidraw/excalidraw/issues/6303)).
- Improved Excalidraw's full-screen mode behavior. Access it via the Obsidian Command Palette or the full-screen button on the Obsidian Tools Panel ([#1528](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1528)).
- Fixed color picker overlapping with the Obsidian mobile toolbar on Obsidian-Mobile.
- Corrected display issues with alternative font sizes (Fibonacci and Zoom relative) in the element properties panel when editing a text element (refer to [2.0.11 Release Notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.11) for details about the font-size Easter Egg).
- Resolved the issue where Excalidraw SVG exports containing LaTeX were not loading correctly into Inkscape ([#1519](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1519)). Thanks to 🙏@HyunggyuJang for the contribution.
`,
"2.0.13":`
## Fixed
- Excalidraw crashes if you paste an image and right-click on canvas immediately after pasting.
`,
"2.0.12":`
## Fixed
- Stencil library not working [#1516](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1516), [#1517](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1517)
- The new convert image from URL to Local File feature did not work in two situations:
- When the embedded image is downloaded from a very slow server (e.g. OpenAIs temp image server)
- On Android
- The postToOpenAI function did not work in all situations on Android.
- ExcaliAI wireframe to code did not display correctly on Android
- Tooltips kept popping up on Android.
## New
- Added "Save image from URL to local file" to the right-click context menu
- Further [ExcaliAI](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md) improvements including support for image editing with image mask
`,
"2.0.11":`
## Fixed
- Resolved an Obsidian performance issue caused by simultaneous installations of Excalidraw and the Minimal theme. Optimized Excalidraw CSS loading into Obsidian since April 2021, resulting in noticeable performance improvements. ([#1456](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1456))
- Removed default support for the [Sliding Panes Plugin](https://github.com/deathau/sliding-panes-obsidian) due to compatibility issues with Obsidian Workspaces. Obsidian's "Stack Tabs" feature now supersedes Sliding Panes. To re-enable sliding panes support, navigate to Compatibility Features in Plugin Settings.
- Sometimes images referenced with URLs did not show in exported scenes and when embedding Excalidraw into a markdown note. I hope all that is now resolved.
- ExcalidrawAutomate scripts sometimes were not able to save their settings.
## New
- Introduced an "Easter Egg" feature in font-size properties:
- Hold SHIFT while selecting font size to use scaled sizes (S, M, L, XL) based on the current canvas zoom, ensuring consistent sizes within zoom ranges.
- Hold ALT/OPT while selecting font size to use values based on the golden mean (s:16, m:26, l:42, xl:68). ALT+SHIFT scales font sizes based on canvas zoom.
- Scaled sizes are sticky; new text elements adjust font sizes relative to the canvas zoom. Deselect SHIFT to disable this feature.
- For more on the Golden Scale, watch [The Golden Opportunity](https://youtu.be/2SHn_ruax-s).
- Added two new Command Palette Actions:
- "Decompress current Excalidraw File" in Markdown View mode helps repair corrupted, compressed Excalidraw files manually.
- "Save image from URL to local file" saves referenced URL images to your Vault, replacing images in the drawing.
- Updated the ExcaliAI script to generate images using ExcaliAI.
## New in ExcalidrawAutomate
- Added additional documentation about functions to ea.suggester.
- Added ea.help(). You can use this function from Developer Console to print help information about functions. Usage: ${String.fromCharCode(96)}ea.help(ea.functionName)${String.fromCharCode(96)} or ${String.fromCharCode(96)}ea.help('propertyName')${String.fromCharCode(96)} - notice property name is in quotes.
`,
"2.0.10":`
One more minor tweak to support an updated ExcaliAI script - now available in the script store.
`,
"2.0.9":`
This release is very minor, and I apologize for the frequent updates in a short span. I chose not to delay this fix for 1-2 weeks, waiting for my larger release. The WireframeToAI feature wasn't working in 2.0.8, but now it does.
`,
"2.0.8":`
## New
- Mermaid Class Diagrams [#7381](https://github.com/excalidraw/excalidraw/pull/7381)
- New Scripts:
- Repeat Texts contributed by @soraliu [#1425](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1425)
- Relative Font Size Cycle [#1474](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1474)
- New setting to configure the URL used to reach the OpenAI API - for setting an OpenAI API compatible local LLM URL.
## Fixed
- web images with jpeg extension were not displayed. [#1486](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1486)
- MathJax was causing errors on the file in the active editor when starting Obsidian or starting the Excalidraw Plugin. I reworked the MathJax implementation from the ground up. [#1484](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1484), [#1473](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1473)
- Enhanced performance for resizing sticky notes (resize + ALT) on slower devices when centrally adjusting their size.
## New in ExcalidrawAutomate:
- New ArrowHead types. Currently only available programmatically and when converting Mermaid Class Diagrams into Excalidraw Objects:
${String.fromCharCode(96,96,96)}ts
addArrow(
points: [x: number, y: number][],
formatting?: {
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
startObjectId?: string;
endObjectId?: string;
},
): string;
connectObjects(
objectA: string,
connectionA: ConnectionPoint | null,
objectB: string,
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): string;
connectObjectWithViewSelectedElement(
objectA: string,
connectionA: ConnectionPoint | null,
connectionB: ConnectionPoint | null,
formatting?: {
numberOfPoints?: number;
startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
padding?: number;
},
): boolean;
${String.fromCharCode(96,96,96)}
`,
"2.0.7":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# Fixed
- The Android and iOS crash with 2.0.5 😰. I can't apologize enough for releasing a version that I did not properly test on Android and iOS. That ought to teach me something about last-minute changes before hitting release.
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
# New
- Bonus feature compared to 2.0.4: Second-order links when clicking embedded images. I use images to connect ideas. Clicking on an image and seeing all the connections immediately is very powerful.
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
- Configurable modifier keys for link click action and drag&drop actions.
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
`,
"2.0.5":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# Fixed
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
# New
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
- Configurable modifier keys for link click action and drag&drop actions.
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
`,
"2.0.4":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- ExcaliAI
- You can now add ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} or ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} to the ${String.fromCharCode(96)}cssclasses:${String.fromCharCode(96)} frontmatter property in embedded markdown nodes and their font face will match the respective Excalidraw fonts.
## Fixed
- Adding a script for the very first time (when the script folder did not yet exist) did not show up in the tools panel. Required an Obsidian restart.
- Performance improvements
## New and updated In Excalidraw Automate
- Added many new functions and some features to existing functions. See the [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.3) for details
`,
"2.0.3":`
## Fixed
- Mermaid to Excalidraw stopped working after installing the Obsidian 1.5.0 insider build. [#1450](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1450)
- CTRL+Click on a Mermaid diagram did not open the Mermaid editor.
- Embed color settings were not honored when the embedded markdown was focused on a section or block.
- Scrollbars were visible when the embeddable was set to transparent (set background color to match element background, and set element background color to "transparent").
`,
"2.0.2":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
## New
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
- Added new functionalities to the active embeddable top-left menu:
- Document Properties (cog icon)
- File renaming
- Basic styling options for embedded markdown documents
- Setting YouTube start time
- Zoom to full screen for PDFs
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
- Introduced new Command Palette Actions:
- Embeddable Properties
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
`,
"2.0.1":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- bug with cssclasses in frontmatter
- styling of help screen keyboard shortcuts [#1437](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1437)
`,
"2.0.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/JC1E-jeiWhI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Added support for applying CSS classes in frontmatter. Now, when embedding Excalidraw drawings into Obsidian Canvas, you can use [Canvas Candy](https://tfthacker.com/canvas-candy) classes. For instance, ${String.fromCharCode(96)}cssclasses: cc-border-none${String.fromCharCode(96)} removes the canvas node border around the drawing.
- Introduced new context menu actions:
- Navigate to link or embedded image.
- Add any file from the vault to the canvas.
- Convert the selected text element or sticky note to an embedded markdown file.
- Add a link from the Vault to the selected element.
- Frames are now rendered in exported images.
- SVG Export includes the ${String.fromCharCode(96)}.excalidraw-svg${String.fromCharCode(96)} class, enabling post-processing of SVGs using publish.js when using custom domains with Obsidian Publish. Also, added a command palette action ${String.fromCharCode(96)}Obsidian Publish: Find SVG and PNG exports that are out of date${String.fromCharCode(96)}.
- Added a new Command palette action to open the corresponding Excalidraw file based on the embedded SVG or PNG file. ${String.fromCharCode(96)}Open Excalidraw Drawing${String.fromCharCode(96)} [Issue #1411](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1411)
## Fixed and Improved
- Resolved issue with the Mermaid Timeline graph displaying all black. [Issue #1424](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1424)
- Enabled toggling pen mode off after activation by a pen touch.
- Now you are able to unlock elements on mobile; previously, locked elements couldn't be selected.
- Fixed the disabled ${String.fromCharCode(96)}complete line button${String.fromCharCode(96)} for multipoint lines on mobile.
![Mobile Editing Image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e7051c75-818f-4800-ba16-ac276e229184)
`,
"1.9.28":`
## Fixed & Improved
- Fixed an issue where the toolbar lost focus, requiring two clicks. This caused a problem when the hand tool was activated from ExcalidrawAutomate script when opening a drawing, causing buttons to stop working. [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
- Resolved a caching issue affecting image area-links and group-links, making them work inconsistently. For more details, refer to the discussion on [Discord](https://discord.com/channels/1026825302900494357/1169311900308361318).
- Improved frame colors with Dynamic Coloring.
- Added support for multiline LaTeX formulas. [#1403](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1403)
- Fixed the issue of Chinese characters overlapping in MathJax. [#1406](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1406)
## New
- Added support for Mermaid to Excalidraw **Sequence Diagrams**.
- If an image contains an element link, clicking on the image will now open the link chooser, allowing you to decide whether to open the image or follow the element link.
- When hovering over an image that also has an element link, the hover preview will display the contents of the link.
- You can now choose to **import PDFs** in columns instead of rows. Additionally, you have the option to group all pages after import, which will improve the unlocking experience if you also lock pages on import.
- Introduced configuration options for the **Laser Tool**, including pointer color, decay length, and time. ([#1408](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1408), [#1220](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1220))
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/c0cad40a-1afc-42af-b41b-f912dd8a4e78)
`,
"1.9.27": `
## New
- Restructured plugin settings, added additional comments and relevant videos
- Added setting to change PDF to Image resolution/scale. This has an effect when embedding PDF pages to Excalidraw. A lower value will result in less-sharp pages, but better overall performance. Also, larger pages (higher scale value) were not accepted by Excalidraw.com when copying from Obsidian due to the 2MB image file limit. Find the "PDF to Image" setting under "Embedding Excalidraw into your Notes and Exporting" setting. [#1393](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1393)
## Fixed
- When multiple Excalidraw Scripts were executed parallel a race condition occurred causing scripts to override each other
- I implemented a partial fix to "text detaching from figures when dragging them" [#1400](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1400)
- Regression: extra thin stroke removed with 1.9.26 [#1399](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1399)`,
"1.9.26":`
## Fixes and improvements from Excalidraw.com
- Freedraw shape selection issue, when fill-pattern is not solid [#7193](https://github.com/excalidraw/excalidraw/pull/7193)
- Actions panel UX improvement [#6850](https://github.com/excalidraw/excalidraw/pull/6850)
## Fixed in plugin
- After inserting PDF pages as image the size of inserted images were incorrectly anchored preventing resizing of pages. The fix does not solve the issue with already imported pages, but pages you import in the future will not be anchored.
- Mobile toolbar flashes up on tab change on desktop
- Toolbar buttons are active on the first click after opening a drawing. This addresses the "hand" issue raised here: [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
`,
"1.9.25":`
## Fixed
- Fixed issues with creating Markdown or Excalidraw files for non-existing documents [#1385](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1385)
- Resolved a bug where changing the section/block filter after duplicating a markdown embeddable now works correctly on the first attempt [#1387](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1387)
## New
- Easily create a markdown file and embed it as an embedded frame with a single click when clicking a link pointing to a non-existent file.
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/4b9de54d-2382-4a52-b500-918ba2a60133)
- Offline LaTeX support. The MathJax package is now included in the plugin, eliminating the need for an internet connection. [#1383](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1383), [#936](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/936), [#1289](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1289)
## Minor Updates from excalidraw.com
- Improved the laser pointer in dark mode.
- Removed bound arrows from frames.
- Enhanced fill rendering.
- Maintained the z-order of elements added to frames.
## New in ExcalidrawAutomate
- Introduced two LZString functions in ExcalidrawAutomate:
${String.fromCharCode(96,96,96)}typescript
compressToBase64(str:string):string;
decompressFromBase64(str:string):string;
${String.fromCharCode(96,96,96)}
`,
"1.9.24":`
## Fixed
- Resolved some hidden Image and Backup Cache initialization errors.
## New Features
- Introducing the ${String.fromCharCode(96)}[[cmd://cmd-id]]${String.fromCharCode(96)} link type, along with a new Command Palette Action: ${String.fromCharCode(96)}Insert Obsidian Command as a link${String.fromCharCode(96)}. With this update, you can now add any command available on the Obsidian Command palette as a link in Excalidraw. When you click the link, the corresponding command will be executed. This feature opens up exciting possibilities for automating your drawings by creating Excalidraw Scripts and attaching them to elements.
- I am thrilled to announce that you can now embed images directly from your local hard drive in Excalidraw. These files won't be moved into Obsidian. Please note, however, that these images won't be synchronized across your other devices. [#1365](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1365)
Check out the [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png)
<a href="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png" width="100%" alt="Keyboard map"/></a>
Stay creative and productive with Excalidraw!
`,
"1.9.23":`
## Fixed
- Link navigation error in view mode introduced with 1.9.21 [#7120](https://github.com/excalidraw/excalidraw/pull/7120)
`,
"1.9.21":`
## Fixed:
- When moving a group of objects on the grid, each object snapped separately resulting in a jumbled-up image [#7082](https://github.com/excalidraw/excalidraw/issues/7082)
## New from Excalidraw.com:
- 🎉 Laser Pointer. Press "K" to activate the laser pointer, or find it under more tools. In View-Mode double click/tap the canvas to toggle the laser pointer
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/d3fc079d-9428-4a93-9a9b-1947ce9b6b57)
`,
"1.9.20":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/QB2rKRxxYlg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Fourth Font displays correctly in SVG embeds mode
- The re-colorMap map (see [1.9.19](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.19) for more info) did not work when either of the fill or stroke color properties of the image was missing.
- Excalidraw Pasting with middle mouse button on Linux [#1338](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1338) 🙏@Aeases
### Fixed by excalidraw.com
- Excalidraw's native eyedropper fixes [#7019](https://github.com/excalidraw/excalidraw/pull/7019)
## New
- Now you can insert [Mermaid](https://mermaid.live/) diagrams as Excalidraw elements into your drawings (currently only the [Flowchart](https://mermaid.js.org/syntax/flowchart.html) type is supported, [other diagram types](https://mermaid.js.org/intro/#diagram-types) are inserted as Mermaid native images.
- ⚠️**This feature requires Obsidian API v1.4.14 (the latest desktop version). On Obsidian mobile API v1.4.14 is only available to Obsidian insiders currently**
- If you want to contribute to the project please head over to [mermaid-to-excalidraw](https://github.com/excalidraw/mermaid-to-excalidraw) and help create the converters for the other diagram types.
- The Fourth Font now also supports the OTF format
- Disable snap-to-grid in grid mode by holding down the CTRL/CMD while drawing or moving an element [#6983](https://github.com/excalidraw/excalidraw/pull/6983)
- I updated the Excalidraw logo in Obsidian. This affects the logo on the tab and the ribbon.
### New from excalidraw.com
- Elements alignment snapping. Hold down the CTRL/CMD button while moving an element to snap it to other objects. [#6256](https://github.com/excalidraw/excalidraw/pull/6256)
### New in the script library
- The amazing shape [Boolean Operations](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Boolean%20Operations.md) script created by 🙏@GColoy is available in the script library.
### New in Excalidraw Automate
- ${String.fromCharCode(96)}getPolyBool()${String.fromCharCode(96)} returns a [PolyBool](https://github.com/velipso/polybooljs) object
- sample mermaid code:
${String.fromCharCode(96,96,96)}js
ea = ExcalidrawAutomate();
ea.setView();
await ea.addMermaid(
${String.fromCharCode(96)}flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]${String.fromCharCode(96)}
);
ea.addElementsToView();
${String.fromCharCode(96,96,96)}`,
"1.9.19":`
## New
- I added new features to the [Deconstruct Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md) script
- I added a new script: [Text Aura](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md)
- I updated the [Set Grid](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Set%20Grid.md) script. You can now set the Major/Minor tick frequency. [#1305](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1305)
- The re-colorMap is now case-insensitive. The color map is a hidden feature. In Markdown View mode you can add a JSON map after the embedded SVG or Excalidraw image filename with a mapping of current colors to new colors.
<img width="100%" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/1d985a59-a2d2-48a2-9cef-686bfbe9ef02"/>
## New in ExcalidrawAutomate
- I added the ${String.fromCharCode(96)}silent${String.fromCharCode(96)} switch. If this is true, the created file will not be opened.
${String.fromCharCode(96,96,96)}typescript
async create(params?: {
filename?: string;
foldername?: string;
templatePath?: string;
onNewPane?: boolean;
silent?: boolean;
frontmatterKeys?: {
"excalidraw-plugin"?: "raw" | "parsed";
"excalidraw-link-prefix"?: string;
"excalidraw-link-brackets"?: boolean;
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string; //text to insert above the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section
}): Promise<string>
${String.fromCharCode(96,96,96)}
`,
"1.9.18":`
## New
- Excalidraw now syncs with Obsidian's language settings, provided translations are available. [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1297)
## Fixed
- [#1285](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1285): Solved Obsidian crashes caused by copying images from Excalidraw into markdown notes. Going forward:
- Copying an image will paste its embed link,
- Copying a text element will paste the text,
- For all other elements with links, the link will be pasted.
- In all other cases nothing will be pasted.
- Resolved grid instability ([#1298](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1298)).
- Fixed missing ${String.fromCharCode(96)}[[square brackets]]${String.fromCharCode(96)} in PDF section references, making the links functional.
- Corrected the behavior of "Open current link in browser" for embedded YouTube and Vimeo frames. Clicking the globe button will now correctly open the links.
`,
"1.9.17":`
## New
- Significant performance improvements from Excalidraw.com
- When selecting a highlight in the Obsidian PDF editor and selecting "Copy as Quote" in the context menu, then paste this to Excalidraw, the text will arrive as a text element wrapped in a transparent sticky note with the link to the original highlight attached to the sticky note. You can override this behavior by SHIFT+CTRL/CMD pasting
## Fixed
- BUG: Image caching issue. Changes to the drawing do not reflect immediately in the note when re-opening the drawing [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1279)
- Removed underline from links in NativeSVG embed.
`,
"1.9.16":`
I apologize for this extra release. I accidentally built 1.9.15 with an older excalidraw.com package version. Fixes and new features (like the improved grid) are now available again. Otherwise, this is the same as 1.9.15. Sorry for the inconvenience.
`,
"1.9.15":`
## New
- There is now a search box in the Excliadraw Script Store. I categorized the scripts and added keywords to help easier navigation.
## Fixed
- The theme of the embedded Markdown document did not always honor plugin settings. With some themes, it worked, with others (including the default Obsidian theme, it didn't).
`,
"1.9.14":`
# Fixed
- **Dynamic Styling**: Excalidraw ${String.fromCharCode(96)}Plugin Settings/Display/Dynamic Styling${String.fromCharCode(96)} did not handle theme changes correctly.
@@ -359,7 +1330,7 @@ ${String.fromCharCode(96,96,96)}`,
## New
- New scripts by @threethan:
- [Auto Draw for Pen](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Auto%20Draw%20for%20Pen.md): Automatically switches between the select and draw tools, based on whether a pen is being used. Supports most pens including Apple Pencil.
- [Hardware Eraser Support](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Hardware%20Eraser%20Support.md): Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen. Supports Windows based styluses. Does not suppoprt Apple Pencil or S-Pen.
- [Hardware Eraser Support](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Hardware%20Eraser%20Support.md): Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen. Supports Windows based styluses. Does not support Apple Pencil or S-Pen.
- Added separate buttons to support copying link, area or group references to objects on the drawing. [#1063](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1063). See [this video](https://youtu.be/yZQoJg2RCKI) for more details on how this works.
- Hover preview will no longer trigger for image files (.png, .svg, .jpg, .gif, .webp, .bmp, .ico, .excalidraw)
- Minor updates to the [Slideshow](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md) script. You can download the updated script from the Excalidraw script library. The slideshow will now correctly run also when initiated in a popout window. When the drawing is in a popout window, the slideshow will not be full screen, but will only occupy the popout window. If you run the slideshow from the main Obsidian workspace, it will be displayed in full-screen mode.

Some files were not shown because too many files have changed in this diff Show More