Compare commits

...

46 Commits

Author SHA1 Message Date
zsviczian
272804afc8 allow jfif and avif 2025-01-14 14:59:47 +00:00
zsviczian
dc0b50f717 Update How-to.yml
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-09 08:52:41 +01:00
zsviczian
a0eb625b8a Update How-to.yml 2025-01-09 08:52:11 +01:00
zsviczian
524dc54d03 Update bug_report.yml 2025-01-09 08:50:35 +01:00
zsviczian
918718be90 Update bug_report.yml 2025-01-09 08:49:59 +01:00
zsviczian
78ee784be1 Update bug_report.yml 2025-01-09 08:48:58 +01:00
zsviczian
7e0e016bf9 Update bug_report.yml 2025-01-09 08:48:18 +01:00
zsviczian
4f875a03a0 2.7.5
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-05 23:09:16 +01:00
zsviczian
63c56e0e98 similar elements allows selection of containers
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 12:49:11 +01:00
zsviczian
46477208be split ellipse and concatenate line now works with rotated lines
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 11:38:45 +01:00
zsviczian
3194c014c7 updated concatenate lines 2025-01-05 10:33:31 +01:00
zsviczian
25ccb9dc43 updated EA lib in docs 2025-01-05 07:03:04 +01:00
zsviczian
fa46f8c39d Merge pull request #1880 from karaolidis/package-lock
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
Allow automated & deterministic builds
2025-01-04 22:18:09 +01:00
zsviczian
8ffe5c3942 2.7.5-beta-1 2025-01-04 21:26:49 +01:00
zsviczian
88f256cd8f Added comments to EA, moved non-class function to EAUtils 2025-01-04 20:23:14 +01:00
zsviczian
1562600cd3 Image mask offset, PDF drift and offset, addAppendUpdateCustomData on EA, some type cleanup
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-04 19:06:43 +01:00
Nikolaos Karaolidis
d759abbc47 Allow commiting package-lock.json
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-01-04 18:38:56 +02:00
zsviczian
90533138e5 fixed embedding images into Excalidraw with areaRef links did not work as expected due to conflicting width and height values
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-31 08:37:46 +01:00
zsviczian
80d8f0e5b6 image occlusion, full-year calendar 2024-12-29 10:52:54 +01:00
zsviczian
9829fab97c Merge pull request #2185 from simonperet/full-year-calendar-script
Add full year calendar generator script
2024-12-29 10:48:30 +01:00
Simon
a33c8b6eab Add a full year calendar generator script 2024-12-29 09:21:44 +01:00
zsviczian
f0921856c1 fix 2184 2024-12-29 07:15:47 +01:00
zsviczian
31e06ac0e0 updated add connector point and set stroke width 2024-12-28 22:19:47 +01:00
zsviczian
033d764b1c 2.7.4 2024-12-28 20:04:22 +01:00
zsviczian
00f98dd14e fix color picker in shade master 2024-12-28 11:14:27 +01:00
zsviczian
0f601d969a updated the script 2024-12-27 22:03:50 +01:00
zsviczian
8fa0fb37b2 shade master mobile friendly(er) 2024-12-27 21:35:57 +01:00
zsviczian
5a58d17d99 2.7.3 2024-12-27 21:04:33 +01:00
zsviczian
982958a4c6 Merge pull request #2179 from dmscode/master
Update zh-cn.ts to d3b61a0
2024-12-27 19:35:52 +01:00
dmscode
d425884bb8 Update zh-cn.ts to d3b61a0 2024-12-27 19:26:02 +08:00
zsviczian
d3b61a0df1 shade master icon corrected 2024-12-27 09:18:12 +01:00
zsviczian
4bab0162ba EA colorMap functions, new duplicate command, minor fileloader fixes, view.loadSceneFiles callback and filewhitelist, publish shade master 1.0 2024-12-27 08:05:41 +01:00
zsviczian
d3f4437478 shade master and new duplicate image command 2024-12-26 23:38:29 +01:00
zsviczian
a64586c3e6 shade master fully functional 2024-12-25 20:44:15 +01:00
zsviczian
7a92e78851 added queue for svg update 2024-12-25 10:10:44 +01:00
zsviczian
af0122b21a store original colors 2024-12-25 01:05:38 +01:00
zsviczian
1f95f57e97 queue and sliders 2024-12-25 00:31:24 +01:00
zsviczian
f384e95e44 modal is draggable 2024-12-24 22:51:52 +01:00
zsviczian
a40521f07b Shade Master v2 2024-12-24 22:36:08 +01:00
zsviczian
9649b36175 shade master v1 2024-12-24 20:05:16 +01:00
zsviczian
6cb1394793 Merge pull request #2176 from dmscode/master
Update zh-cn.ts to 22d3f25
2024-12-24 08:27:19 +01:00
dmscode
e5b2977c0c Update zh-cn.ts to 22d3f25
And remove spaces from line end
2024-12-24 08:18:04 +08:00
zsviczian
22d3f25dc4 renderingConcurrency; createSliderWithText 2024-12-23 21:15:06 +01:00
zsviczian
d9534fcc4f Fixed: toggleImageAnchoring 2024-12-23 20:04:33 +01:00
zsviczian
fd1604c3a4 slideshow script will now remember last slide on multiple presentations within the same session when starting slideshow holding down the SHIFT Modifier key 2024-12-23 08:56:06 +01:00
zsviczian
8f0f8d64df colors to lower case 2024-12-22 10:33:43 +01:00
57 changed files with 20719 additions and 1950 deletions

View File

@@ -12,6 +12,8 @@ body:
Before submitting a support request, please:
1. **Review the [documentation](https://github.com/zsviczian/obsidian-excalidraw-plugin/wiki)** your question may already be answered.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if your question has already been addressed.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -31,6 +33,13 @@ body:
options:
- label: Yes, I have reviewed the documentation and searched for related issues.
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: support_question
attributes:

View File

@@ -1,5 +1,5 @@
name: Bug report
description: If something is clearly broken, its a bug. Everything else is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
description: If something is clearly broken, its a bug. **Everything else** is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
title: "BUG: "
body:
- type: markdown
@@ -12,6 +12,8 @@ body:
Before creating a bug report, please:
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** maybe there is already an answer.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -46,6 +48,13 @@ body:
description: "Run `Command Palette/Show Debug info` in Obsidian and paste the result here."
placeholder: "Paste your Obsidian debug info here..."
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: bug_description
attributes:

2
.gitignore vendored
View File

@@ -4,7 +4,6 @@
# npm
node_modules
package-lock.json
# build
main.js
@@ -14,6 +13,7 @@ hot-reload.bat
data.json
lib
dist
tmp
#VSCode
.vscode

2
.nvmrc
View File

@@ -1 +1 @@
18
18

1342
MathjaxToSVG/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,8 @@
"cross-env": "^7.0.3",
"obsidian": "1.5.7-1",
"rollup": "^2.70.1",
"typescript": "^5.2.2",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-terser": "^7.0.2",
"tslib": "^2.8.1",
"typescript": "^5.2.2"
}
}
}

View File

@@ -16,10 +16,10 @@ export default {
},
plugins: [
typescript({
tsconfig: '../tsconfig.json',
tsconfig: 'tsconfig.json',
}),
commonjs(),
nodeResolve({
nodeResolve({
browser: true,
preferBuiltins: false
}),
@@ -32,4 +32,4 @@ export default {
}
})
].filter(Boolean)
};
};

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"resolveJsonModule": true,
"lib": [
"dom",
"scripthost",
"es2022",
"DOM.Iterable"
],
"jsx": "react",
},
"include": [
"**/*.ts",
"**/*.tsx", "src/shared/Dialogs/OpenDrawing.ts",
"src/types/types.d.ts",
]
}

View File

@@ -1,4 +0,0 @@
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:

File diff suppressed because it is too large Load Diff

5782
docs/Release-notes.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,4 +22,4 @@ elements.forEach((el)=>{
);
ea.addToGroup([el.id,ellipseId]);
});
ea.addElementsToView(false,false);
await ea.addElementsToView(false,false,true);

View File

@@ -8,20 +8,76 @@ if(lines.length !== 2) {
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,
];
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}
const points = lines.map(
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
const points = lines.map(getNormalizedLine).map(
el=>el.points.map(p=>[p[0]+el.x, p[1]+el.y])
);
const last = (p) => p[p.length-1];
@@ -99,4 +155,4 @@ switch (lineTypes) {
}
ea.addElementsToView();
await ea.addElementsToView();

View File

@@ -9,7 +9,7 @@ Select some elements in the scene. The script will take these elements and move
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -81,6 +81,7 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const path = (img?.linkParts?.original)??(img?.file?.path);
const hyperlink = img?.hyperlink;
if(img && (path || hyperlink)) {
const colorMap = ea.getColorMapForImageElement(el);
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
@@ -90,6 +91,7 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
hyperlink,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
colorMap,
};
return;
}

View File

@@ -0,0 +1,157 @@
/*
This script generates a complete calendar for a specified year, visually distinguishing weekends from weekdays through color coding.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-exemple.excalidraw.png)
## Customizable Colors
You can personalize the calendars appearance by defining your own colors:
1. Create two rectangles in your design.
2. Select both rectangles before running the script:
• The **fill and stroke colors of the first rectangle** will be applied to weekdays.
• The **fill and stroke colors of the second rectangle** will be used for weekends.
If no rectangle are selected, the default color schema will be used (white and purple).
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-customize.excalidraw.png)
```javascript
*/
ea.reset();
// -------------------------------------
// Constants initiation
// -------------------------------------
const RECT_WIDTH = 300; // day width
const RECT_HEIGHT = 45; // day height
const START_X = 0; // X start position
const START_Y = 0; // PY start position
const MONTH_SPACING = 30; // space between months
const DAY_SPACING = 0; // space between days
const DAY_NAME_SPACING = 45; // space between day number and day letters
const DAY_NAME_AND_NUMBER_X_MARGIN = 5;
const MONTH_NAME_SPACING = -40;
const YEAR_X = (RECT_WIDTH + MONTH_SPACING) * 6 - 150;
const YEAR_Y = -200;
let COLOR_WEEKEND = "#c3abf3";
let COLOR_WEEKDAY = "#ffffff";
const COLOR_DAY_STROKE = "none";
let STROKE_DAY = 4;
let FILLSTYLE_DAY = "solid";
const FONT_SIZE_MONTH = 60;
const FONT_SIZE_DAY = 30;
const FONT_SIZE_YEAR = 100;
const LINE_STROKE_SIZE = 4;
let LINE_STROKE_COLOR_WEEKDAY = "black";
let LINE_STROKE_COLOR_WEEKEND = "black";
const SATURDAY = 6;
const SUNDAY = 0;
const JANUARY = 0;
const FIRST_DAY_OF_THE_MONTH = 1;
const DAY_NAME_AND_NUMBER_Y_MARGIN = (RECT_HEIGHT - FONT_SIZE_DAY) / 2;
// -------------------------------------
// ask for requested Year
// Default value is the current year
let requestedYear = parseFloat(new Date().getFullYear());
requestedYear = parseFloat(await utils.inputPrompt("Year ?", requestedYear, requestedYear));
if(isNaN(requestedYear)) {
new Notice("Invalid number");
return;
}
// -------------------------------------
// Use selected element for the calendar style
// -------------------------------------
let elements = ea.getViewSelectedElements();
if (elements.length>=1){
COLOR_WEEKDAY = elements[0].backgroundColor;
FILLSTYLE_DAY = elements[0].fillStyle;
STROKE_DAY = elements[0].strokeWidth;
LINE_STROKE_COLOR_WEEKDAY = elements[0].strokeColor;
}
if (elements.length>=2){
COLOR_WEEKEND = elements[1].backgroundColor;
LINE_STROKE_COLOR_WEEKEND = elements[1].strokeColor;
}
// get the first day of the current year (01/01)
var firstDayOfYear = new Date(requestedYear, JANUARY, FIRST_DAY_OF_THE_MONTH);
var currentDay = firstDayOfYear
// write year number
let calendarYear = firstDayOfYear.getFullYear();
ea.style.fontSize = FONT_SIZE_YEAR;
ea.addText(START_X + YEAR_X, START_Y + YEAR_Y, String(calendarYear));
// while we do not reach the end of the year iterate on all the day of the current year
do {
var curentDayOfTheMonth = currentDay.getDate();
var currentMonth = currentDay.getMonth();
var isWeekend = currentDay.getDay() == SATURDAY || currentDay.getDay() == SUNDAY;
// set background color if it's a weekend or weekday
ea.style.backgroundColor = isWeekend ? COLOR_WEEKEND : COLOR_WEEKDAY ;
ea.style.strokeColor = COLOR_DAY_STROKE;
ea.style.fillStyle = FILLSTYLE_DAY;
ea.style.strokeWidth = STROKE_DAY;
let x = START_X + currentMonth * (RECT_WIDTH + MONTH_SPACING);
let y = START_Y + curentDayOfTheMonth * (RECT_HEIGHT + DAY_SPACING);
// only one time per month
if(curentDayOfTheMonth == FIRST_DAY_OF_THE_MONTH) {
// add month name
ea.style.fontSize = FONT_SIZE_MONTH;
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, START_Y+MONTH_NAME_SPACING, currentDay.toLocaleString('default', { month: 'long' }));
}
// Add day rectangle
ea.style.fontSize = FONT_SIZE_DAY;
ea.addRect(x, y, RECT_WIDTH, RECT_HEIGHT);
// set stroke color based on weekday
ea.style.strokeColor = isWeekend ? LINE_STROKE_COLOR_WEEKEND : LINE_STROKE_COLOR_WEEKDAY;
// add line between days
//ea.style.strokeColor = LINE_STROKE_COLOR_WEEKDAY;
ea.style.strokeWidth = LINE_STROKE_SIZE;
ea.addLine([[x,y],[x+RECT_WIDTH, y]]);
// add day number
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(curentDayOfTheMonth));
// add day name
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN + DAY_NAME_SPACING, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(currentDay.toLocaleString('default', { weekday: 'narrow' })));
// go to the next day
currentDay.setDate(currentDay.getDate() + 1);
} while (!(currentDay.getMonth() == JANUARY && currentDay.getDate() == FIRST_DAY_OF_THE_MONTH)) // stop if we reach the 01/01 of the next year
await ea.addElementsToView(false, false, true);

View File

@@ -0,0 +1,10 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50.097575068473816 56.100231877975375" width="50.097575068473816" height="56.100231877975375" class="excalidraw-svg">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAN4AA8AAAAABswAAAMeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgQwcLgZgP1NUQVREAEQRCAqCMIIHCwoAATYCJAMQBCAFhCQHIBuUBcguChxjano4VFxM2vQ1QySkcTXuPsXzT/vRzn0z64qIV7yJJ/EmGSp5Q0EalEiqlgiVyv/L7uv1Qpy/pAfsqfk9Cx44ly4UHRVC2VW+YH51qZj/wAD1eZu/7e/zfzNtkm0LNEs8kV4W4FwgUb7BEox+1s0hcpt7B9o7UD9zWyypoLCgyLAoF3TIGFjCt/9zgmlAiWgimHRUDsvrVQ0d4PV6RpPA690oUcCLFQz/C/KW1hSwQxBdGRfjymFWuCiNpQ7DZwDDscLwyUUTpYlomhJRStJMOi7BBt/PBqzAjvfKyhfW1JZFD4AbvUueQVDCsDDk3yTfBN7BFQ3t838A/UISdgP6BED+1nvsZim+dJYbtD/zgeUIAj7ZZBGCWGbFzydiAggCGUFfAAoNyywFy6ycBsbpAlxRrmEgQGPIthJUmvOuGydAT4H3YPkMSmTbaOxSp3F7M1DceuB47Z4/v7F+08G4H9CV65T/cP2u2BPnHPNo1Njby9l9lqCzm7uyMRu86fNiz8HY2fFxdzcm+/lrt24Fx+vVjuOWhqzPHlHBaqZuP3PXoc7G406+6sZfeO7ufn05DaoJRR5e5HzVrTsP79AzLra5Dm2pRJjYVsGw41e6W67j9sQLnUPzqc0Fimnx1xZHPf0V1+aX3CiUZM329mTN07WNyR3+eb++hXAzh+fUMLjgc9WH8Z3yyaPxrSryLvf0qvAGgKB9DTasXLeHXQv+jTfLj/DT99Yu4E/arcz/tgqsErobMKpAeLRyF0C2LRAeQ88XaXVqd92A/IrQCaF7wtobVgIAumDKFlhKtwe+w49BxmkfyDLrB9lcN1numByxaYAYdVJSCoFpewlG+CpsjU9+k4kD7sNkoxSNaBN4OlktYpSEN64bjcfiEE10Ch6BVZpGaEY1oGqiArsLTWOeiiko6ZJkSZEm3HxNmjWpzFMZbutn6SSjtL1mKvApcDlMNUNDv0uTIlUGSgcOjVL8TAsNJqCNIyildAQH05hRYnAIQmWWJ1kyFl8W6cYkGYfJCxXDbbqExsAUhFky5ZIfyxKLDU8LAgAA); }
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(17.077535418245503 32.086027259866626) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.865850031647774 39.07185193521212) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.946763254178506 46.743160072731996) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.680839244467208 53.831041680294504) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.41441860670051 32.80666516147113) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.20273322010279 39.79248983681666) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g transform="translate(0 0) rotate(0 25.048787534236908 17.010119812063635)"><text x="0" y="25.30097820935094" font-family="Nunito, Segoe UI Emoji" font-size="25.200177499353526px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CAL</text></g><g stroke-linecap="round"><g transform="translate(26.079917947816256 27.03803518092093) rotate(0 0 14.531098348527223)"><path d="M0 0 C0 4.84, 0 24.22, 0 29.06 M0 0 C0 4.84, 0 24.22, 0 29.06" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.199941306131535 47.15190785557627) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -7,29 +7,58 @@ This script enables the selection of elements based on matching properties. Sele
```js */
let config = window.ExcalidrawSelectConfig;
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
const isValidConfig = config && (Date.now() - config.timestamp < 60000);
config = isValidConfig ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
new Notice("Select a single element");
return;
} else {
if(elements.length === 0) {
elements = ea.getViewElements();
if(!config) {
async function shouldAbort() {
if(elements.length === 1) return false;
if(elements.length !== 2) return true;
//maybe container?
const textEl = elements.find(el=>el.type==="text");
if(!textEl || !textEl.containerId) return true;
const containerEl = elements.find(el=>el.id === textEl.containerId);
if(!containerEl) return true;
const id = await utils.suggester(
elements.map(el=>el.type),
elements.map(el=>el.id),
"Select container component"
);
if(!id) return true;
elements = elements.filter(el=>el.id === id);
return false;
}
if(await shouldAbort()) {
new Notice("Select a single element");
return;
}
}
if(Boolean(config) && elements.length === 0) {
elements = ea.getViewElements();
}
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));
function lc(x) {
return x?.toLocaleLowerCase();
}
//--------------------------
// RUN
//--------------------------
const run = () => {
selectedElements = elements.filter(el=>
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
((typeof config.backgroundColor === "undefined") || (lc(el.backgroundColor) === lc(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)) &&
@@ -38,7 +67,7 @@ const run = () => {
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
((typeof config.strokeColor === "undefined") || (el.strokeColor === config.strokeColor)) &&
((typeof config.strokeColor === "undefined") || (lc(el.strokeColor) === lc(config.strokeColor))) &&
((typeof config.strokeStyle === "undefined") || (el.strokeStyle === config.strokeStyle)) &&
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
((typeof config.type === "undefined") || (el.type === config.type)) &&

View File

@@ -17,4 +17,5 @@ if(isNaN(width)) {
const elements=ea.getViewSelectedElements();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>el.strokeWidth=width);
ea.addElementsToView(false,false);
await ea.addElementsToView(false,false);
ea.viewUpdateScene({appState: {currentItemStrokeWidth: width}});

729
ea-scripts/Shade Master.md Normal file
View File

@@ -0,0 +1,729 @@
/*
This is an experimental script. If you find bugs, please consider debugging yourself then submitting a PR on github with the fix, instead of raising an issue. Thank you!
This script modifies the color lightness/hue/saturation/transparency of selected Excalidraw elements and SVG and nested Excalidraw drawings. Select eligible elements in the scene, then run the script.
- The color of Excalidraw elements (lines, ellipses, rectangles, etc.) will be changed by the script.
- The color of SVG elements and nested Excalidraw drawings will only be mapped. When mapping colors, the original image remains unchanged, only a mapping table is created and the image is recolored during rendering of your Excalidraw screen. In case you want to make manual changes you can also edit the mapping in Markdown View Mode under `## Embedded Files`
If you select only a single SVG or nested Excalidraw element, then the script offers an additional feature. You can map colors one by one in the image.
```js*/
const HELP_TEXT = `
<ul>
<li dir="auto">Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements</li>
<li dir="auto">For a single selected image, you can map colors individually in the color mapping section</li>
<li dir="auto">For Excalidraw elements: stroke and background colors are modified permanently</li>
<li dir="auto">For SVG/nested drawings: original files stay unchanged, color mapping is stored under <code>## Embedded Files</code></li>
<li dir="auto">Using color maps helps maintain links between drawings while allowing different color themes</li>
<li dir="auto">Sliders work on relative scale - the amount of change is applied to current values</li>
<li dir="auto">Unlike Excalidraw's opacity setting which affects the whole element:
<ul>
<li dir="auto">Shade Master can set different opacity for stroke vs background</li>
<li dir="auto">Note: SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"</li>
<li dir="auto">Additionally if the same color is used as fill and stroke the color can only be mapped once</li>
</ul>
</li>
<li dir="auto">This is an experimental script - contributions welcome on GitHub via PRs</li>
</ul>
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.2")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
/*
SVGColorInfo is returned by ea.getSVGColorInfoForImgElement. Color info will all the color strings in the SVG file plus "fill" which represents the default fill color for SVG icons set at the SVG root element level. Fill if not set defaults to black:
type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
In the Excalidraw file under `## Embedded Files` the color map is included after the file. That color map implements ColorMap. ea.updateViewSVGImageColorMap takes a ColorMap as input.
interface ColorMap {
[color: string]: string;
};
*/
// Main script execution
const allElements = ea.getViewSelectedElements();
const svgImageElements = allElements.filter(el => {
if(el.type !== "image") return false;
const file = ea.getViewFileForImageElement(el);
if(!file) return false;
return el.type === "image" && (
file.extension === "svg" ||
ea.isExcalidrawFile(file)
);
});
if(allElements.length === 0) {
new Notice("Select at least one rectangle, ellipse, diamond, line, arrow, freedraw, text or SVG image elment");
return;
}
const originalColors = new Map();
const currentColors = new Map();
const colorInputs = new Map();
const sliderResetters = [];
let terminate = false;
const FORMAT = "Color Format";
const STROKE = "Modify Stroke Color";
const BACKGROUND = "Modify Background Color"
const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"];
const precision = [1,2,2,3];
const minLigtness = 1/Math.pow(10,precision[2]);
const maxLightness = 100 - minLigtness;
const minSaturation = 1/Math.pow(10,precision[2]);
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings[STROKE]) {
settings = {};
settings[FORMAT] = {
value: "HEX",
valueset: ["HSL", "RGB", "HEX"],
description: "Output color format."
};
settings[STROKE] = { value: true }
settings[BACKGROUND] = {value: true }
ea.setScriptSettings(settings);
}
function getRegularElements() {
ea.clear();
//loading view elements again as element objects change when colors are updated
const allElements = ea.getViewSelectedElements();
return allElements.filter(el =>
["rectangle", "ellipse", "diamond", "line", "arrow", "freedraw", "text"].includes(el.type)
);
}
const updatedImageElementColorMaps = new Map();
let isWaitingForSVGUpdate = false;
function updateViewImageColors() {
if(terminate || isWaitingForSVGUpdate || updatedImageElementColorMaps.size === 0) {
return;
}
isWaitingForSVGUpdate = true;
elementArray = Array.from(updatedImageElementColorMaps.keys());
colorMapArray = Array.from(updatedImageElementColorMaps.values());
updatedImageElementColorMaps.clear();
ea.updateViewSVGImageColorMap(elementArray, colorMapArray).then(()=>{
isWaitingForSVGUpdate = false;
updateViewImageColors();
});
}
async function storeOriginalColors() {
// Store colors for regular elements
for (const el of getRegularElements()) {
const key = el.id;
const colorData = {
type: "regular",
strokeColor: el.strokeColor,
backgroundColor: el.backgroundColor
};
originalColors.set(key, colorData);
}
// Store colors for SVG elements
for (const el of svgImageElements) {
const colorInfo = await ea.getSVGColorInfoForImgElement(el);
const svgColors = new Map();
for (const [color, info] of colorInfo.entries()) {
svgColors.set(color, {...info});
}
originalColors.set(el.id, {type: "svg",colors: svgColors});
}
copyOriginalsToCurrent();
}
function copyOriginalsToCurrent() {
for (const [key, value] of originalColors.entries()) {
if(value.type === "regular") {
currentColors.set(key, {...value});
} else {
const newColorMap = new Map();
for (const [color, info] of value.colors.entries()) {
newColorMap.set(color, {...info});
}
currentColors.set(key, {type: "svg", colors: newColorMap});
}
}
}
function clearSVGMapping() {
for (const resetter of sliderResetters) {
resetter();
}
// Reset SVG elements
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
current.colors.get(color).mappedTo = color;
}
}
} else {
for (const el of svgImageElements) {
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
current.colors.get(color).mappedTo = color;
}
}
}
}
run("clear");
}
// Set colors
async function setColors(colors) {
debounceColorPicker = true;
const regularElements = getRegularElements();
if (regularElements.length > 0) {
ea.copyViewElementsToEAforEditing(regularElements);
for (const el of ea.getElements()) {
const original = colors.get(el.id);
if (original && original.type === "regular") {
if (original.strokeColor) el.strokeColor = original.strokeColor;
if (original.backgroundColor) el.backgroundColor = original.backgroundColor;
}
}
await ea.addElementsToView(false, false);
}
// Reset SVG elements
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
for (const [color, info] of original.colors.entries()) {
newColorMap[color] = info.mappedTo;
// Update UI components
const inputs = colorInputs.get(color);
if (inputs) {
if(info.mappedTo === "fill") {
info.mappedTo = "black";
//"fill" is a special value in case the SVG has no fill color defined (i.e black)
inputs.textInput.setValue("black");
inputs.colorPicker.setValue("#000000");
} else {
const cm = ea.getCM(info.mappedTo);
inputs.textInput.setValue(info.mappedTo);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
updatedImageElementColorMaps.set(el, newColorMap);
}
} else {
for (const el of svgImageElements) {
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
for (const [color, info] of original.colors.entries()) {
newColorMap[color] = info.mappedTo;
}
updatedImageElementColorMaps.set(el, newColorMap);
}
}
}
updateViewImageColors();
}
function modifyColor(color, isDecrease, step, action) {
if (!color) return null;
const cm = ea.getCM(color);
if (!cm) return color;
let modified = cm;
if (modified.lightness === 0) modified = modified.lightnessTo(minLigtness);
if (modified.lightness === 100) modified = modified.lightnessTo(maxLightness);
if (modified.saturation === 0) modified = modified.saturationTo(minSaturation);
switch(action) {
case "Lightness":
// handles edge cases where lightness is 0 or 100 would convert saturation and hue to 0
let lightness = cm.lightness;
const shouldRoundLight = (lightness === minLigtness || lightness === maxLightness);
if (shouldRoundLight) lightness = Math.round(lightness);
lightness += isDecrease ? -step : step;
if (lightness <= 0) lightness = minLigtness;
if (lightness >= 100) lightness = maxLightness;
modified = modified.lightnessTo(lightness);
break;
case "Hue":
modified = isDecrease ? modified.hueBy(-step) : modified.hueBy(step);
break;
case "Transparency":
modified = isDecrease ? modified.alphaBy(-step) : modified.alphaBy(step);
break;
default:
let saturation = cm.saturation;
const shouldRoundSat = saturation === minSaturation;
if (shouldRoundSat) saturation = Math.round(saturation);
saturation += isDecrease ? -step : step;
if (saturation <= 0) saturation = minSaturation;
modified = modified.saturationTo(saturation);
}
const hasAlpha = modified.alpha < 1;
const opts = { alpha: hasAlpha, precision };
const format = settings[FORMAT].value;
switch(format) {
case "RGB": return modified.stringRGB(opts).toLowerCase();
case "HEX": return modified.stringHEX(opts).toLowerCase();
default: return modified.stringHSL(opts).toLowerCase();
}
}
function slider(contentEl, action, min, max, step, invert) {
let prevValue = (max-min)/2;
let debounce = false;
let sliderControl;
new ea.obsidian.Setting(contentEl)
.setName(action)
.addSlider(slider => {
sliderControl = slider;
slider
.setLimits(min, max, step)
.setValue(prevValue)
.onChange(async (value) => {
if (debounce) return;
const isDecrease = invert ? value > prevValue : value < prevValue;
const step = Math.abs(value-prevValue);
prevValue = value;
if(step>0) {
run(action, isDecrease, step);
}
});
}
);
return () => {
debounce = true;
prevValue = (max-min)/2;
sliderControl.setValue(prevValue);
debounce = false;
}
}
function showModal() {
let debounceColorPicker = true;
const modal = new ea.obsidian.Modal(app);
let dirty = false;
modal.onOpen = async () => {
const { contentEl, modalEl } = modal;
const { width, height } = ea.getExcalidrawAPI().getAppState();
modal.bgOpacity = 0;
contentEl.createEl('h2', { text: 'Shade Master' });
const helpDiv = contentEl.createEl("details", {
attr: { style: "margin-bottom: 1em;background: var(--background-secondary); padding: 1em; border-radius: 4px;" }});
helpDiv.createEl("summary", { text: "Help & Usage Guide", attr: { style: "cursor: pointer; color: var(--text-accent);" } });
const helpDetailsDiv = helpDiv.createEl("div", {
attr: { style: "margin-top: 0em; " }
});
helpDetailsDiv.innerHTML = HELP_TEXT;
const component = new ea.obsidian.Setting(contentEl)
.setName(FORMAT)
.setDesc("Output color format")
.addDropdown(dropdown => dropdown
.addOptions({
"HSL": "HSL",
"RGB": "RGB",
"HEX": "HEX"
})
.setValue(settings[FORMAT].value)
.onChange(value => {
settings[FORMAT].value = value;
run();
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName(STROKE)
.addToggle(toggle => toggle
.setValue(settings[STROKE].value)
.onChange(value => {
settings[STROKE].value = value;
dirty = true;
})
);
new ea.obsidian.Setting(contentEl)
.setName(BACKGROUND)
.addToggle(toggle => toggle
.setValue(settings[BACKGROUND].value)
.onChange(value => {
settings[BACKGROUND].value = value;
dirty = true;
})
);
// lightness and saturation are on a scale of 0%-100%
// Hue is in degrees, 360 for the full circle
// transparency is on a range between 0 and 1 (equivalent to 0%-100%)
// The range for lightness, saturation and transparency are double since
// the input could be at either end of the scale
// The range for Hue is 360 since regarless of the position on the circle moving
// the slider to the two extremes will travel the entire circle
// To modify blacks and whites, lightness first needs to be changed to value between 1% and 99%
sliderResetters.push(slider(contentEl, "Hue", 0, 360, 1, false));
sliderResetters.push(slider(contentEl, "Saturation", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Lightness", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Transparency", 0, 2, 0.05, true));
// Add color pickers if a single SVG image is selected
if (svgImageElements.length === 1) {
const svgElement = svgImageElements[0];
//note that the objects in currentColors might get replaced when
//colors are reset, thus in the onChange functions I will always
//read currentColorInfo from currentColors based on svgElement.id
const initialColorInfo = currentColors.get(svgElement.id).colors;
const colorSection = contentEl.createDiv();
colorSection.createEl('h3', { text: 'SVG Colors' });
for (const [color, info] of initialColorInfo.entries()) {
const row = new ea.obsidian.Setting(colorSection)
.setName(color === "fill" ? "SVG default" : color)
.setDesc(`${info.fill ? "Fill" : ""}${info.fill && info.stroke ? " & " : ""}${info.stroke ? "Stroke" : ""}`);
row.descEl.style.width = "100px";
row.nameEl.style.width = "100px";
// Create color preview div
const previewDiv = row.controlEl.createDiv();
previewDiv.style.width = "50px";
previewDiv.style.height = "20px";
previewDiv.style.border = "1px solid var(--background-modifier-border)";
if (color === "transparent") {
previewDiv.style.backgroundImage = "linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)";
previewDiv.style.backgroundSize = "10px 10px";
previewDiv.style.backgroundPosition = "0 0, 0 5px, 5px -5px, -5px 0px";
} else {
previewDiv.style.backgroundColor = ea.getCM(color).stringHEX({alpha: false}).toLowerCase();
}
const resetButton = new ea.obsidian.Setting(row.controlEl)
.addButton(button => button
.setButtonText(">>")
.setClass("reset-color-button")
.onClick(async () => {
const original = originalColors.get(svgElement.id);
const current = currentColors.get(svgElement.id);
if (original?.type === "svg") {
const originalInfo = original.colors.get(color);
const currentInfo = current.colors.get(color);
if (originalInfo) {
currentInfo.mappedTo = color;
run("reset single color");
}
}
}))
resetButton.settingEl.style.padding = "0";
resetButton.settingEl.style.border = "0";
// Add text input for color value
const textInput = new ea.obsidian.TextComponent(row.controlEl)
.setValue(info.mappedTo)
.setPlaceholder("Color value");
textInput.inputEl.style.width = "100%";
textInput.onChange(value => {
const lower = value.toLowerCase();
if (lower === color) return;
textInput.setValue(lower);
})
const applyButtonComponent = new ea.obsidian.Setting(row.controlEl)
.addButton(button => button
.setIcon("check")
.setTooltip("Apply")
.onClick(async () => {
const value = textInput.getValue();
try {
if(!CSS.supports("color",value)) {
new Notice (`${value} is not a valid color string`);
return;
}
const cm = ea.getCM(value);
if (cm) {
const format = settings[FORMAT].value;
const alpha = cm.alpha < 1 ? true : false;
const newColor = format === "RGB"
? cm.stringRGB({alpha , precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision }).toLowerCase();
textInput.setValue(newColor);
const currentInfo = currentColors.get(svgElement.id).colors;
currentInfo.get(color).mappedTo = newColor;
run("Update SVG color");
debounceColorPicker = true;
colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
} catch (e) {
console.error("Invalid color value:", e);
}
}));
applyButtonComponent.settingEl.style.padding = "0";
applyButtonComponent.settingEl.style.border = "0";
// Add color picker
const colorPicker = new ea.obsidian.ColorComponent(row.controlEl)
.setValue(ea.getCM(info.mappedTo).stringHEX({alpha: false}).toLowerCase());
colorPicker.colorPickerEl.style.maxWidth = "2.5rem";
// Store references to the components
colorInputs.set(color, {
textInput,
colorPicker,
previewDiv,
resetButton
});
colorPicker.colorPickerEl.addEventListener('click', () => {
debounceColorPicker = false;
});
colorPicker.onChange(async (value) => {
try {
if(!debounceColorPicker) {
const currentInfo = currentColors.get(svgElement.id).colors.get(color);
// Preserve alpha from original color
const originalAlpha = ea.getCM(currentInfo.mappedTo).alpha;
const cm = ea.getCM(value);
cm.alphaTo(originalAlpha);
const alpha = originalAlpha < 1 ? true : false;
const format = settings[FORMAT].value;
const newColor = format === "RGB"
? cm.stringRGB({alpha, precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision }).toLowerCase();
// Update text input
textInput.setValue(newColor);
// Update SVG
currentInfo.mappedTo = newColor;
run("Update SVG color");
}
} catch (e) {
console.error("Invalid color value:", e);
} finally {
debounceColorPicker = true;
}
});
}
}
const buttons = new ea.obsidian.Setting(contentEl);
if(svgImageElements.length > 0) {
buttons.addButton(button => button
.setButtonText("Initialize SVG Colors")
.onClick(() => {
debounceColorPicker = true;
clearSVGMapping();
})
);
}
buttons
.addButton(button => button
.setButtonText("Reset")
.onClick(() => {
for (const resetter of sliderResetters) {
resetter();
}
copyOriginalsToCurrent();
setColors(originalColors);
}))
.addButton(button => button
.setButtonText("Close")
.setCta(true)
.onClick(() => modal.close()));
makeModalDraggable(modalEl);
const maxHeight = Math.round(height * 0.6);
const maxWidth = Math.round(width * 0.9);
modalEl.style.maxHeight = `${maxHeight}px`;
modalEl.style.maxWidth = `${maxWidth}px`;
};
modal.onClose = () => {
terminate = true;
if (dirty) {
ea.setScriptSettings(settings);
}
if(ea.targetView.isDirty()) {
ea.targetView.save(false);
}
};
modal.open();
}
/**
* Add draggable functionality to the modal element.
* @param {HTMLElement} modalEl - The modal element to make draggable.
*/
function makeModalDraggable(modalEl) {
let isDragging = false;
let startX, startY, initialX, initialY;
const header = modalEl.querySelector('.modal-titlebar') || modalEl; // Default to modalEl if no titlebar
header.style.cursor = 'move';
const onPointerDown = (e) => {
// Ensure the event target isn't an interactive element like slider, button, or input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = modalEl.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
modalEl.style.position = 'absolute';
modalEl.style.margin = '0';
modalEl.style.left = `${initialX}px`;
modalEl.style.top = `${initialY}px`;
};
const onPointerMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
modalEl.style.left = `${initialX + dx}px`;
modalEl.style.top = `${initialY + dy}px`;
};
const onPointerUp = () => {
isDragging = false;
};
header.addEventListener('pointerdown', onPointerDown);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
// Clean up event listeners on modal close
modalEl.addEventListener('remove', () => {
header.removeEventListener('pointerdown', onPointerDown);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
});
}
function executeChange(isDecrease, step, action) {
const modifyStroke = settings[STROKE].value;
const modifyBackground = settings[BACKGROUND].value;
const regularElements = getRegularElements();
// Process regular elements
if (regularElements.length > 0) {
for (const el of regularElements) {
const currentColor = currentColors.get(el.id);
if (modifyStroke && currentColor.strokeColor) {
currentColor.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action);
}
if (modifyBackground && currentColor.backgroundColor) {
currentColor.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action);
}
}
}
// Process SVG image elements
if (svgImageElements.length === 1) { // Only update UI for single SVG
const el = svgImageElements[0];
colorInfo = currentColors.get(el.id).colors;
// Process each color in the SVG
for (const [color, info] of colorInfo.entries()) {
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
if (shouldModify) {
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
colorInfo.get(color).mappedTo = modifiedColor;
// Update UI components if they exist
const inputs = colorInputs.get(color);
if (inputs) {
const cm = ea.getCM(modifiedColor);
inputs.textInput.setValue(modifiedColor);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
} else {
if (svgImageElements.length > 0) {
for (const el of svgImageElements) {
const colorInfo = currentColors.get(el.id).colors;
// Process each color in the SVG
for (const [color, info] of colorInfo.entries()) {
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
if (shouldModify) {
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
colorInfo.get(color).mappedTo = modifiedColor;
}
}
}
}
}
}
let isRunning = false;
let queue = false;
function processQueue() {
if (!terminate && !isRunning && queue) {
queue = false;
isRunning = true;
setColors(currentColors).then(() => {
isRunning = false;
if (queue) processQueue();
});
}
}
function run(action="Hue", isDecrease=true, step=0) {
// passing invalid action (such as "clear") will bypass rewriting of colors using CM
// this is useful when resetting colors to original values
if(ACTIONS.includes(action)) {
executeChange(isDecrease, step, action);
}
queue = true;
if (!isRunning) processQueue();
}
await storeOriginalColors();
showModal();
processQueue();

View File

@@ -0,0 +1 @@
<svg class="skip" 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"><path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -26,6 +26,10 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
return;
}
if(ea.targetView.isDirty()) {
ea.targetView.forceSave(true);
}
const hostLeaf = ea.targetView.leaf;
const hostView = hostLeaf.view;
const statusBarElement = document.querySelector("div.status-bar");
@@ -33,7 +37,7 @@ const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierK
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")
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")
//-------------------------------
//constants
//-------------------------------
@@ -57,8 +61,9 @@ const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
//-------------------------------
//utility & convenience functions
//-------------------------------
let shouldSaveAfterThePresentation = false;
let isLaserOn = false;
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide : 0;
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] : 0;
let isFullscreen = false;
const ownerDocument = ea.targetView.ownerDocument;
const startFullscreen = !altKey;
@@ -350,8 +355,8 @@ 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;
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")) {
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = slide;
}
}
@@ -505,6 +510,7 @@ const createPresentationNavigationPanel = () => {
new ea.obsidian.ToggleComponent(el)
.setValue(isHidden)
.onChange(value => {
shouldSaveAfterThePresentation = true;
if(value) {
excalidrawAPI.setToast({
message:"The presentation path remain hidden after the presentation. No need to select the line again. Just click the slideshow button to start the next presentation.",
@@ -730,6 +736,9 @@ const exitPresentation = async (openForEdit = false) => {
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
if(!shouldSaveAfterThePresentation) {
ea.targetView.clearDirty();
}
}
//--------------------------
@@ -755,6 +764,7 @@ const start = async () => {
resetControlPanelElPosition();
}
if(presentationPathType === "line") await toggleArrowVisibility(isHidden);
ea.targetView.clearDirty();
}
const timestamp = Date.now();
@@ -769,10 +779,14 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
delete window.ExcalidrawSlideshowStartTimer;
}
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
timestamp,
slide: 0
};
if(!window.ExcalidrawSlideshow) {
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
slide: {},
};
}
window.ExcalidrawSlideshow.timestamp = timestamp;
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = 0;
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}

View File

@@ -18,6 +18,7 @@ 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");
lines = lines.map(getNormalizedLine);
const subLines = getSubLines(lines);
const angles = subLines.flatMap(line => {
@@ -206,3 +207,70 @@ function isBetween(num, min, max) {
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}

File diff suppressed because one or more lines are too long

View File

@@ -94,6 +94,7 @@ 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/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/Shade%20Master.svg"/></div>|[[#Shade Master]]|
|<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]]|
@@ -147,6 +148,7 @@ 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/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]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.svg"/></div>|[[#Full-Year Calendar Generator]]|
## Masking and cropping
**Keywords**: Crop, Mask, Transform images
@@ -393,6 +395,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/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>
## Full-Year Calendar Generator
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/simonperet'>@simonperet</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/Full-Year%20Calendar%20Generator.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Generates a complete calendar for a specified year.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-exemple.excalidraw.png'></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
@@ -563,6 +571,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/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/Set%20Text%20Alignment.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg'></td></tr></table>
## Shade Master
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.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/Shade%20Master.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can modify the colors of SVG images, embedded files, and Excalidraw elements in a drawing by changing Hue, Saturation, Lightness and Transparency; and if only a single SVG or nested Excalidraw drawing is selected, then you can remap image colors.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Slideshow
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

View File

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

View File

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

9290
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-22",
"@zsviczian/excalidraw": "0.17.6-25",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",
@@ -58,7 +58,7 @@
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-typescript": "^12.1.2",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/js-yaml": "^4.0.9",
@@ -79,10 +79,10 @@
"rollup-plugin-copy": "^3.5.0",
"@zsviczian/rollup-plugin-postprocess": "^1.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-typescript2": "^0.36.0",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2",
"typescript": "^5.7.3",
"fs-extra": "^11.2.0",
"uglify-js": "^3.19.3"
},

View File

@@ -198,7 +198,7 @@ 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 IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif", "jfif", "avif"];
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;

View File

@@ -39,7 +39,8 @@ import {
setRootElementSize,
} from "../constants/constants";
import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings";
import { initExcalidrawAutomate, ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
import { initExcalidrawAutomate } from "src/utils/excalidrawAutomateUtils";
import { around, dedupe } from "monkey-around";
import { t } from "../lang/helpers";
import {
@@ -91,6 +92,15 @@ import { EventManager } from "./managers/EventManager";
declare const PLUGIN_VERSION:string;
declare const INITIAL_TIMESTAMP: number;
type FileMasterInfo = {
isHyperLink: boolean;
isLocalLink: boolean;
path: string;
hasSVGwithBitmap: boolean;
blockrefData: string,
colorMapJSON?: string
}
export default class ExcalidrawPlugin extends Plugin {
private fileManager: PluginFileManager;
private observerManager: ObserverManager;
@@ -113,7 +123,7 @@ export default class ExcalidrawPlugin extends Plugin {
public opencount: number = 0;
public ea: ExcalidrawAutomate;
//A master list of fileIds to facilitate copy / paste
public filesMaster: Map<FileId, { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
public filesMaster: Map<FileId, FileMasterInfo> =
null; //fileId, path
public equationsMaster: Map<FileId, string> = null; //fileId, formula
public mermaidsMaster: Map<FileId, string> = null; //fileId, mermaidText

View File

@@ -16,6 +16,7 @@ import {
IMAGE_TYPES,
DEVICE,
sceneCoordsToViewportCoords,
fileid,
} from "../../constants/constants";
import ExcalidrawView, { TextMode } from "../../view/ExcalidrawView";
import {
@@ -28,11 +29,8 @@ import { InsertCommandDialog } from "../../shared/Dialogs/InsertCommandDialog";
import { InsertImageDialog } from "../../shared/Dialogs/InsertImageDialog";
import { ImportSVGDialog } from "../../shared/Dialogs/ImportSVGDialog";
import { InsertMDDialog } from "../../shared/Dialogs/InsertMDDialog";
import {
ExcalidrawAutomate,
insertLaTeXToView,
search,
} from "../../shared/ExcalidrawAutomate";
import { ExcalidrawAutomate } from "../../shared/ExcalidrawAutomate";
import { insertLaTeXToView, search } from "src/utils/excalidrawAutomateUtils";
import { templatePromt } from "../../shared/Dialogs/Prompt";
import { t } from "../../lang/helpers";
import {
@@ -53,7 +51,7 @@ import {
getImageSize,
} from "../../utils/utils";
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, isObsidianThemeDark, mergeMarkdownFiles, setExcalidrawView } from "../../utils/obsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ReleaseNotes } from "../../shared/Dialogs/ReleaseNotes";
import { ScriptInstallPrompt } from "../../shared/Dialogs/ScriptInstallPrompt";
import Taskbone from "../../shared/OCR/Taskbone";
@@ -71,6 +69,7 @@ import { carveOutImage, carveOutPDF, createImageCropperFile } from "../../utils/
import { showFrameSettings } from "../../shared/Dialogs/FrameSettings";
import { insertImageToView } from "../../utils/excalidrawViewUtils";
import ExcalidrawPlugin from "src/core/main";
import { get } from "http";
declare const PLUGIN_VERSION:string;
@@ -1044,6 +1043,43 @@ export class CommandManager {
}
})
this.addCommand({
id: "duplicate-image",
name: t("DUPLICATE_IMAGE"),
checkCallback: (checking:boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
if(els.length !== 1) {
if(checking) return false;
new Notice("Select a single image element and try again");
return false;
}
const el = els[0] as ExcalidrawImageElement;
const ef = view.excalidrawData.getFile(el.fileId);
if(!ef?.file) return false;
if(checking) return true;
(async()=>{
const ea = getEA(view) as ExcalidrawAutomate;
const isAnchored = Boolean(el.customData?.isAnchored);
const imgId = await ea.addImage(el.x+el.width/5, el.y+el.height/5, ef.file,!isAnchored, isAnchored);
const img = ea.getElement(imgId) as Mutable<ExcalidrawImageElement>;
img.width = el.width;
img.height = el.height;
if(el.crop) img.crop = {...el.crop};
const newFileId = fileid() as FileId;
ea.imagesDict[newFileId] = ea.imagesDict[img.fileId];
ea.imagesDict[newFileId].id = newFileId;
delete ea.imagesDict[img.fileId]
img.fileId = newFileId;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView([imgId]);
ea.destroy();
})();
}
})
this.addCommand({
id: "reset-image-to-100",
name: t("RESET_IMG_TO_100"),

View File

@@ -8,7 +8,7 @@ import {
} from "obsidian";
import { DEVICE, RERENDER_EVENT } from "../../constants/constants";
import { EmbeddedFilesLoader } from "../../shared/EmbeddedFileLoader";
import { createPNG, createSVG } from "../../shared/ExcalidrawAutomate";
import { createPNG, createSVG } from "../../utils/excalidrawAutomateUtils";
import { ExportSettings } from "../../view/ExcalidrawView";
import ExcalidrawPlugin from "../main";
import {getIMGFilename,} from "../../utils/fileUtils";
@@ -893,7 +893,12 @@ export const markdownPostProcessor = async (
el.firstElementChild?.hasClass("frontmatter") ||
el.firstElementChild?.hasClass("block-language-yaml");
if(isPrinting && isFrontmatter) {
return;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
const isDrawing = file && file instanceof TFile && plugin.isExcalidrawFile(file);
if(isDrawing) {
return;
}
}
//@ts-ignore

View File

@@ -41,6 +41,7 @@ import { Rank } from "src/constants/actionIcons";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
export interface ExcalidrawSettings {
folder: string;
@@ -70,6 +71,7 @@ export interface ExcalidrawSettings {
annotatePreserveSize: boolean;
displaySVGInPreview: boolean; //No longer used since 1.9.13
previewImageType: PreviewImageType; //Introduced with 1.9.13
renderingConcurrency: number;
allowImageCache: boolean;
allowImageCacheInScene: boolean;
displayExportedImageIfAvailable: boolean;
@@ -248,6 +250,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
annotatePreserveSize: false,
displaySVGInPreview: undefined,
previewImageType: undefined,
renderingConcurrency: 3,
allowImageCache: true,
allowImageCacheInScene: true,
displayExportedImageIfAvailable: false,
@@ -1053,55 +1056,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h1",
});
new Setting(detailsEl)
.setName(t("DEFAULT_PEN_MODE_NAME"))
.setDesc(fragWithHTML(t("DEFAULT_PEN_MODE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("never", "Never")
.addOption("mobile", "On Obsidian Mobile")
.addOption("always", "Always")
.setValue(this.plugin.settings.defaultPenMode)
.onChange(async (value: "never" | "always" | "mobile") => {
this.plugin.settings.defaultPenMode = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeSingleFingerPanning)
.onChange(async (value) => {
this.plugin.settings.penModeSingleFingerPanning = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeCrosshairVisible)
.onChange(async (value) => {
this.plugin.settings.penModeCrosshairVisible = value;
this.applySettingsUpdate();
}),
);
const readingModeEl = new Setting(detailsEl)
.setName(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME"))
.setDesc(fragWithHTML(t("SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC")))
@@ -1328,27 +1282,77 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let zoomText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("ZOOM_TO_FIT_MAX_LEVEL_NAME"))
.setDesc(fragWithHTML(t("ZOOM_TO_FIT_MAX_LEVEL_DESC")))
.addSlider((slider) =>
slider
.setLimits(0.5, 10, 0.5)
.setValue(this.plugin.settings.zoomToFitMaxLevel)
.onChange(async (value) => {
zoomText.innerText = ` ${value.toString()}`;
this.plugin.settings.zoomToFitMaxLevel = value;
createSliderWithText(detailsEl, {
name: t("ZOOM_TO_FIT_MAX_LEVEL_NAME"),
desc: t("ZOOM_TO_FIT_MAX_LEVEL_DESC"),
value: this.plugin.settings.zoomToFitMaxLevel,
min: 0.5,
max: 10,
step: 0.5,
onChange: (value) => {
this.plugin.settings.zoomToFitMaxLevel = value;
this.applySettingsUpdate();
}
})
// ------------------------------------------------
// Pen
// ------------------------------------------------
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("PEN_HEAD"),
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("DEFAULT_PEN_MODE_NAME"))
.setDesc(fragWithHTML(t("DEFAULT_PEN_MODE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("never", "Never")
.addOption("mobile", "On Obsidian Mobile")
.addOption("always", "Always")
.setValue(this.plugin.settings.defaultPenMode)
.onChange(async (value: "never" | "always" | "mobile") => {
this.plugin.settings.defaultPenMode = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
zoomText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.zoomToFitMaxLevel.toString()}`;
});
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeSingleFingerPanning)
.onChange(async (value) => {
this.plugin.settings.penModeSingleFingerPanning = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeCrosshairVisible)
.onChange(async (value) => {
this.plugin.settings.penModeCrosshairVisible = value;
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Grid
@@ -1397,28 +1401,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
// Grid opacity slider (hex value between 00 and FF)
let opacityValue: HTMLDivElement;
new Setting(detailsEl)
.setName(t("GRID_OPACITY_NAME"))
.setDesc(fragWithHTML(t("GRID_OPACITY_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 100, 1) // 0 to 100 in decimal
.setValue(this.plugin.settings.gridSettings.OPACITY)
.onChange(async (value) => {
opacityValue.innerText = ` ${value.toString()}`;
this.plugin.settings.gridSettings.OPACITY = value;
this.applySettingsUpdate();
updateGridColor();
}),
)
.settingEl.createDiv("", (el) => {
opacityValue = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.gridSettings.OPACITY}`;
});
createSliderWithText(detailsEl, {
name: t("GRID_OPACITY_NAME"),
desc: t("GRID_OPACITY_DESC"),
value: this.plugin.settings.gridSettings.OPACITY,
min: 0,
max: 100,
step: 1,
onChange: (value) => {
this.plugin.settings.gridSettings.OPACITY = value;
this.applySettingsUpdate();
updateGridColor();
},
minWidth: "3em",
})
// ------------------------------------------------
// Laser Pointer
@@ -1439,47 +1435,33 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let decayTime: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LASER_DECAY_TIME_NAME"))
.setDesc(fragWithHTML(t("LASER_DECAY_TIME_DESC")))
.addSlider((slider) =>
slider
.setLimits(500, 20000, 500)
.setValue(this.plugin.settings.laserSettings.DECAY_TIME)
.onChange(async (value) => {
decayTime.innerText = ` ${value.toString()}`;
this.plugin.settings.laserSettings.DECAY_TIME = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
decayTime = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.laserSettings.DECAY_TIME.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LASER_DECAY_TIME_NAME"),
desc: t("LASER_DECAY_TIME_DESC"),
value: this.plugin.settings.laserSettings.DECAY_TIME,
min: 500,
max: 20000,
step: 500,
onChange: (value) => {
this.plugin.settings.laserSettings.DECAY_TIME = value;
this.applySettingsUpdate();
},
minWidth: "3em",
})
let decayLength: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LASER_DECAY_LENGTH_NAME"))
.setDesc(fragWithHTML(t("LASER_DECAY_LENGTH_DESC")))
.addSlider((slider) =>
slider
.setLimits(25, 2000, 25)
.setValue(this.plugin.settings.laserSettings.DECAY_LENGTH)
.onChange(async (value) => {
decayLength.innerText = ` ${value.toString()}`;
this.plugin.settings.laserSettings.DECAY_LENGTH = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
decayLength = el;
el.style.minWidth = "3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.laserSettings.DECAY_LENGTH.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LASER_DECAY_LENGTH_NAME"),
desc: t("LASER_DECAY_LENGTH_DESC"),
value: this.plugin.settings.laserSettings.DECAY_LENGTH,
min: 25,
max: 2000,
step: 25,
onChange: (value) => {
this.plugin.settings.laserSettings.DECAY_LENGTH = value;
this.applySettingsUpdate();
},
minWidth: "3em",
})
detailsEl = displayDetailsEl.createEl("details");
detailsEl.createEl("summary", {
@@ -1488,47 +1470,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
detailsEl.createDiv({ text: t("DRAG_MODIFIER_DESC"), cls: "setting-item-description" });
let longPressDesktop: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LONG_PRESS_DESKTOP_NAME"))
.setDesc(fragWithHTML(t("LONG_PRESS_DESKTOP_DESC")))
.addSlider((slider) =>
slider
.setLimits(300, 3000, 100)
.setValue(this.plugin.settings.longPressDesktop)
.onChange(async (value) => {
longPressDesktop.innerText = ` ${value.toString()}`;
this.plugin.settings.longPressDesktop = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
longPressDesktop = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.longPressDesktop.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LONG_PRESS_DESKTOP_NAME"),
desc: t("LONG_PRESS_DESKTOP_DESC"),
value: this.plugin.settings.longPressDesktop,
min: 300,
max: 3000,
step: 100,
onChange: (value) => {
this.plugin.settings.longPressDesktop = value;
this.applySettingsUpdate(true);
},
})
let longPressMobile: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LONG_PRESS_MOBILE_NAME"))
.setDesc(fragWithHTML(t("LONG_PRESS_MOBILE_DESC")))
.addSlider((slider) =>
slider
.setLimits(300, 3000, 100)
.setValue(this.plugin.settings.longPressMobile)
.onChange(async (value) => {
longPressMobile.innerText = ` ${value.toString()}`;
this.plugin.settings.longPressMobile = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
longPressMobile = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.longPressMobile.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LONG_PRESS_MOBILE_NAME"),
desc: t("LONG_PRESS_MOBILE_DESC"),
value: this.plugin.settings.longPressMobile,
min: 300,
max: 3000,
step: 100,
onChange: (value) => {
this.plugin.settings.longPressMobile = value;
this.applySettingsUpdate(true);
},
})
new Setting(detailsEl)
.setName(t("DOUBLE_CLICK_LINK_OPEN_VIEW_MODE"))
@@ -1700,26 +1666,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
);
donePrefixSetting.setDisabled(!this.plugin.settings.parseTODO);
let opacityText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("LINKOPACITY_NAME"))
.setDesc(fragWithHTML(t("LINKOPACITY_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 1, 0.05)
.setValue(this.plugin.settings.linkOpacity)
.onChange(async (value) => {
opacityText.innerText = ` ${value.toString()}`;
this.plugin.settings.linkOpacity = value;
this.applySettingsUpdate(true);
}),
)
.settingEl.createDiv("", (el) => {
opacityText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.linkOpacity.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("LINKOPACITY_NAME"),
desc: t("LINKOPACITY_DESC"),
value: this.plugin.settings.linkOpacity,
min: 0,
max: 1,
step: 0.05,
onChange: (value) => {
this.plugin.settings.linkOpacity = value;
this.applySettingsUpdate(true);
},
});
new Setting(detailsEl)
.setName(t("HOVERPREVIEW_NAME"))
@@ -1953,6 +1911,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h3",
});
createSliderWithText(detailsEl, {
name: t("RENDERING_CONCURRENCY_NAME"),
desc: t("RENDERING_CONCURRENCY_DESC"),
min: 1,
max: 5,
step: 1,
value: this.plugin.settings.renderingConcurrency,
onChange: (value) => {
this.plugin.settings.renderingConcurrency = value;
this.applySettingsUpdate();
}
});
new Setting(detailsEl)
.setName(t("EMBED_IMAGE_CACHE_NAME"))
.setDesc(fragWithHTML(t("EMBED_IMAGE_CACHE_DESC")))
@@ -2077,49 +2048,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
let scaleText: HTMLDivElement;
createSliderWithText(detailsEl, {
name: t("EXPORT_PNG_SCALE_NAME"),
desc: t("EXPORT_PNG_SCALE_DESC"),
value: this.plugin.settings.pngExportScale,
min: 1,
max: 5,
step: 0.5,
onChange: (value) => {
this.plugin.settings.pngExportScale = value;
this.applySettingsUpdate();
}
});
new Setting(detailsEl)
.setName(t("EXPORT_PNG_SCALE_NAME"))
.setDesc(fragWithHTML(t("EXPORT_PNG_SCALE_DESC")))
.addSlider((slider) =>
slider
.setLimits(1, 5, 0.5)
.setValue(this.plugin.settings.pngExportScale)
.onChange(async (value) => {
scaleText.innerText = ` ${value.toString()}`;
this.plugin.settings.pngExportScale = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
scaleText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.pngExportScale.toString()}`;
});
let exportPadding: HTMLDivElement;
new Setting(detailsEl)
.setName(t("EXPORT_PADDING_NAME"))
.setDesc(fragWithHTML(t("EXPORT_PADDING_DESC")))
.addSlider((slider) =>
slider
.setLimits(0, 50, 5)
.setValue(this.plugin.settings.exportPaddingSVG)
.onChange(async (value) => {
exportPadding.innerText = ` ${value.toString()}`;
this.plugin.settings.exportPaddingSVG = value;
this.applySettingsUpdate();
}),
)
.settingEl.createDiv("", (el) => {
exportPadding = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.exportPaddingSVG.toString()}`;
});
createSliderWithText(detailsEl, {
name: t("EXPORT_PADDING_NAME"),
desc: fragWithHTML(t("EXPORT_PADDING_DESC")),
value: this.plugin.settings.exportPaddingSVG,
min: 0,
max: 50,
step: 5,
onChange: (value) => {
this.plugin.settings.exportPaddingSVG = value;
this.applySettingsUpdate();
}
});
detailsEl = exportDetailsEl.createEl("details");
detailsEl.createEl("summary", {
@@ -2460,27 +2413,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
cls: "excalidraw-setting-h3",
});
let areaZoomText: HTMLDivElement;
new Setting(detailsEl)
.setName(t("MAX_IMAGE_ZOOM_IN_NAME"))
.setDesc(fragWithHTML(t("MAX_IMAGE_ZOOM_IN_DESC")))
.addSlider((slider) =>
slider
.setLimits(1, 10, 0.5)
.setValue(this.plugin.settings.areaZoomLimit)
.onChange(async (value) => {
areaZoomText.innerText = ` ${value.toString()}`;
this.plugin.settings.areaZoomLimit = value;
this.applySettingsUpdate();
this.plugin.excalidrawConfig.updateValues(this.plugin);
}),
)
.settingEl.createDiv("", (el) => {
areaZoomText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.plugin.settings.areaZoomLimit.toString()}`;
createSliderWithText(detailsEl, {
name: t("MAX_IMAGE_ZOOM_IN_NAME"),
desc: fragWithHTML(t("MAX_IMAGE_ZOOM_IN_DESC")),
value: this.plugin.settings.areaZoomLimit,
min: 1,
max: 10,
step: 0.5,
onChange: (value) => {
this.plugin.settings.areaZoomLimit = value;
this.applySettingsUpdate();
this.plugin.excalidrawConfig.updateValues(this.plugin);
},
});
detailsEl = nonstandardDetailsEl.createEl("details");

View File

@@ -30,6 +30,7 @@ export default {
"Script is up to date - Click to reinstall",
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
DUPLICATE_IMAGE: "Duplicate selected image with a different image ID",
CONVERT_NOTE_TO_EXCALIDRAW: "Convert markdown note to Excalidraw Drawing",
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
CREATE_NEW: "Create new drawing",
@@ -416,6 +417,7 @@ FILENAME_HEAD: "Filename",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
PEN_HEAD: "Pen",
GRID_HEAD: "Grid",
GRID_DYNAMIC_COLOR_NAME: "Dynamic grid color",
GRID_DYNAMIC_COLOR_DESC:
@@ -578,7 +580,11 @@ FILENAME_HEAD: "Filename",
EMBED_CANVAS_DESC:
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
EMBED_CACHING: "Image caching",
EMBED_CACHING: "Image caching and rendering optimization",
RENDERING_CONCURRENCY_NAME: "Image rendering concurrency",
RENDERING_CONCURRENCY_DESC:
"Number of parallel workers to use for image rendering. Increasing this number will speed up the rendering process, but may slow down the rest of the system. " +
"The default value is 3. You can increase this number if you have a powerful system.",
EXPORT_SUBHEAD: "Export Settings",
EMBED_SIZING: "Image sizing",
EMBED_THEME_BACKGROUND: "Image theme and background color",

View File

@@ -30,6 +30,7 @@ export default {
"脚本已是最新 - 点击重新安装",
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
DUPLICATE_IMAGE : "复制选定的图像,并分配一个不同的图像 ID",
CONVERT_NOTE_TO_EXCALIDRAW: "转换:空白 Markdown 文档 => Excalidraw 绘图文件",
CONVERT_EXCALIDRAW: "转换: *.excalidraw => *.md",
CREATE_NEW: "新建绘图文件",
@@ -196,7 +197,7 @@ export default {
NEWVERSION_NOTIFICATION_DESC:
"<b>开启:</b>当本插件存在可用更新时,显示通知。<br>" +
"<b>关闭:</b>您需要手动检查本插件的更新(设置 - 第三方插件 - 检查更新)。",
BASIC_HEAD: "基本",
BASIC_DESC: `包括:更新说明,更新提示,新绘图文件、模板文件、脚本文件的存储路径等的设置。`,
FOLDER_NAME: "Excalidraw 文件夹(區分大小寫!)",
@@ -406,7 +407,7 @@ FILENAME_HEAD: "文件名",
DEFAULT_WHEELZOOM_NAME: "鼠标滚轮缩放页面",
DEFAULT_WHEELZOOM_DESC:
`<b>开启:</b>鼠标滚轮为缩放页面,${labelCTRL()}+鼠标滚轮为滚动页面</br><b>关闭:</b>鼠标滚轮为滚动页面,${labelCTRL()}+鼠标滚轮为缩放页面`,
ZOOM_TO_FIT_NAME: "调节面板尺寸后自动缩放页面",
ZOOM_TO_FIT_DESC: "调节面板尺寸后,自适应地缩放页面" +
"<br><b>开启:</b>自动缩放。<br><b>关闭:</b>禁用自动缩放。",
@@ -416,6 +417,7 @@ FILENAME_HEAD: "文件名",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.550%)且不能超过 101000%)。",
PEN_HEAD: "手写笔",
GRID_HEAD: "网格",
GRID_DYNAMIC_COLOR_NAME: "动态网格颜色",
GRID_DYNAMIC_COLOR_DESC:
@@ -575,10 +577,14 @@ FILENAME_HEAD: "文件名",
此外,还有自动导出 SVG 或 PNG 文件并保持与绘图文件状态同步的设置。`,
EMBED_CANVAS: "Obsidian 白板支持",
EMBED_CANVAS_NAME: "沉浸式嵌入",
EMBED_CANVAS_DESC:
EMBED_CANVAS_DESC:
"当嵌入绘图到 Obsidian 白板中时,隐藏元素的边界和背景。" +
"注意:如果想要背景完全透明,您依然需要在 Excalidraw 中设置“导出的图像不包含背景”。",
EMBED_CACHING: "预览图缓存",
EMBED_CACHING : "图像缓存和渲染优化" ,
RENDERING_CONCURRENCY_NAME : "图像渲染并发性" ,
RENDERING_CONCURRENCY_DESC :
"用于图像渲染的并行工作线程数。增加此数值可以加快渲染速度,但可能会减慢系统的其他部分运行速度。" +
"默认值为 3。如果您的系统性能强大可以增加此数值。" ,
EXPORT_SUBHEAD: "导出",
EMBED_SIZING: "图像尺寸",
EMBED_THEME_BACKGROUND: "图像的主题和背景色",
@@ -586,7 +592,7 @@ FILENAME_HEAD: "文件名",
EMBED_IMAGE_CACHE_DESC: "可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
SCENE_IMAGE_CACHE_NAME: "缓存场景中嵌套的 Excalidraw",
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
SCENE_IMAGE_CACHE_DESC: "缓存场景中嵌套的 Excalidraw 以加快场景渲染速度。这将加快渲染过程,特别是在您的场景中有深度嵌套的 Excalidraw 时。" +
"Excalidraw 将智能地尝试识别嵌套 Excalidraw 的子元素是否发生变化,并更新缓存。 " +
"如果您怀疑缓存未能正确更新,您可能需要关闭此功能。",
EMBED_IMAGE_CACHE_CLEAR: "清除缓存",
@@ -630,7 +636,7 @@ FILENAME_HEAD: "文件名",
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_MARKDOWN_COMMENT_NAME: "将链接作为注释嵌入",
EMBED_MARKDOWN_COMMENT_DESC:
EMBED_MARKDOWN_COMMENT_DESC:
"在图像下方以 Markdown 链接的形式嵌入原始 Excalidraw 文件的链接,例如:<code>%%[[drawing.excalidraw]]%%</code>。<br>" +
"除了添加 Markdown 注释之外,您还可以选择嵌入的 SVG 或 PNG并使用命令面板" +
"'<code>Excalidraw: 打开 Excalidraw 绘图</code>'来打开该绘图",
@@ -709,7 +715,7 @@ FILENAME_HEAD: "文件名",
"文件浏览器等创建的绘图都将是旧格式(*.excalidraw。" +
"此外,您打开旧格式绘图文件时将不再收到警告消息。",
MATHJAX_NAME: "MathJax (LaTeX) 的 javascript 库服务器",
MATHJAX_DESC: "如果您在绘图中使用 LaTeX插件需要从服务器获取并加载一个 javascript 库。" +
MATHJAX_DESC: "如果您在绘图中使用 LaTeX插件需要从服务器获取并加载一个 javascript 库。" +
"如果您的网络无法访问某些库服务器,可以尝试通过此选项更换库服务器。"+
"更改此选项后,您可能需要重启 Obsidian 来使其生效。",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
@@ -728,7 +734,7 @@ FILENAME_HEAD: "文件名",
EXPERIMENTAL_HEAD: "杂项",
EXPERIMENTAL_DESC: `包括:默认的 LaTeX 公式字段建议绘图文件的类型标识符OCR 等设置。`,
EA_HEAD: "Excalidraw 自动化",
EA_DESC:
EA_DESC:
"ExcalidrawAutomate 是用于 Excalidraw 自动化脚本的 API但是目前说明文档还不够完善" +
"建议阅读 <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> 文件源码," +
"参考 <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> 网页(不过该网页" +
@@ -791,7 +797,7 @@ FILENAME_HEAD: "文件名",
CJK_ASSETS_FOLDER_NAME: "CJK 字体文件夹(區分大小寫!)",
CJK_ASSETS_FOLDER_DESC: `您可以在此设置 CJK 字体文件夹的位置。例如,您可以选择将其放置在 <code>Excalidraw/CJK Fonts</code> 下。<br><br>
<strong>重要:</strong> 请勿将此文件夹设置为 Vault 根目录!请勿在此文件夹中放置其他字体。<br><br>
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备之间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备之间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
LOAD_CHINESE_FONTS_NAME: "启动时从文件加载中文字体",
LOAD_JAPANESE_FONTS_NAME: "启动时从文件加载日文字体",
LOAD_KOREAN_FONTS_NAME: "启动时从文件加载韩文字体",
@@ -806,7 +812,7 @@ FILENAME_HEAD: "文件名",
TASKBONE_ENABLE_DESC: "启用意味着您同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>。",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
"到 <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a> 购买一个商用 API key。购买后请将它填写到旁边这个文本框里替换掉原本自动生成的免费 API key。",

View File

@@ -4,11 +4,36 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { Notice } from "obsidian";
import { getEA } from "src/core";
import { ExcalidrawAutomate, cloneElement } from "src/shared/ExcalidrawAutomate";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { cloneElement } from "src/utils/excalidrawAutomateUtils";
import { ExportSettings } from "src/view/ExcalidrawView";
import { nanoid } from "src/constants/constants";
import { svgToBase64 } from "../utils/utils";
/**
* Creates a masked image from an Excalidraw scene.
*
* The scene must contain:
* - One element.type="frame" element that defines the crop area
* - One or more element.type="image" elements
* - Zero or more non-image shape elements (rectangles, ellipses etc) that define the mask
*
* The class splits the scene into two parts:
* 1. Images (managed in imageEA)
* 2. Mask shapes (managed in maskEA)
*
* A transparent rectangle matching the combined bounding box is added to both
* imageEA and maskEA to ensure consistent sizing between image and mask.
*
* For performance, if there is only one image, it is not rotated, and
* its size matches the bounding box,
* the image data is used directly from cache rather than regenerating.
*
* @example
* const cropper = new CropImage(elements, files);
* const pngBlob = await cropper.getCroppedPNG();
* cropper.destroy();
*/
export class CropImage {
private imageEA: ExcalidrawAutomate;
private maskEA: ExcalidrawAutomate;
@@ -106,10 +131,15 @@ export class CropImage {
withTheme: false,
isMask: false,
}
const isRotated = this.imageEA.getElements().some(el=>el.type === "image" && el.angle !== 0);
const images = Object.values(this.imageEA.imagesDict);
if(!isRotated && (images.length === 1)) {
return images[0].dataURL;
const images = this.imageEA.getElements().filter(el=>el.type === "image" && el.isDeleted === false);
const isRotated = images.some(el=>el.angle !== 0);
const imageDataURLs = Object.values(this.imageEA.imagesDict);
if(!isRotated && images.length === 1 && imageDataURLs.length === 1) {
const { width, height } = this.bbox;
if(images[0].width === width && images[0].height === height) {
//get image from the cache if mask is not bigger than the image, and if there is a single image element
return imageDataURLs[0].dataURL;
}
}
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
}

View File

@@ -17,6 +17,107 @@ 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://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.7.5":`
## Fixed
- PDF export scenario described in [#2184](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184)
- Elbow arrows do not work within frames [#2187](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2187)
- Embedding images into Excalidraw with areaRef links did not work as expected due to conflicting SVG viewbox and width and height values
- Can't exit full-screen mode in popout windows using the Command Palette toggle action [#2188](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2188)
- If the image mask extended beyond the image in "Mask and Crop" image mode, the mask got misaligned from the image.
- PDF image embedding fixes that impacted some PDF files (not all):
- When cropping the PDF page in the scene (by double-clicking the image to crop), the size and position of the PDF cutout drifted.
- Using PDF++ there was a small offset in the position of the cutout in PDF++ and the image in Excalidraw.
- Updated a number of scripts including Split Ellipse, Select Similar Elements, and Concatenate Lines
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}
/**
* Add, modify, or delete keys in element.customData and preserve existing keys.
* Creates customData={} if it does not exist.
* Takes the element id for an element in ea.elementsDict and the newData to add or modify.
* To delete keys set key value in newData to undefined. So {keyToBeDeleted:undefined} will be deleted.
* @param id
* @param newData
* @returns undefined if element does not exist in elementsDict, returns the modified element otherwise.
*/
public addAppendUpdateCustomData(id:string, newData: Partial<Record<string, unknown>>);
${String.fromCharCode(96,96,96)}
`,
"2.7.4":`
## Fixed
- Regression from 2.7.3 where image fileId got overwritten in some cases
- White flash when opening a dark drawing [#2178](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2178)
`,
"2.7.3":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Toggling image size anchoring on and off by modifying the image link did not update the image in the view until the user forced saved it or closed and opened the drawing again. This was a side-effect of the less frequent view save introduced in 2.7.1
## New
- **Shade Master Script**: A new script that allows you to modify the color lightness, hue, saturation, and transparency of selected Excalidraw elements, SVG images, and nested Excalidraw drawings. When a single image is selected, you can map colors individually. The original image remains unchanged, and a mapping table is added under ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} for SVG and nested drawings. This helps maintain links between drawings while allowing different color themes.
- New Command Palette Command: "Duplicate selected image with a different image ID". Creates a copy of the selected image with a new image ID. This allows you to add multiple color mappings to the same image. In the scene, the image will be treated as if a different image, but loaded from the same file in the Vault.
## QoL Improvements
- New setting under ${String.fromCharCode(96)}Embedding Excalidraw into your notes and Exporting${String.fromCharCode(96)} > ${String.fromCharCode(96)}Image Caching and rendering optimization${String.fromCharCode(96)}. You can now set the number of concurrent workers that render your embedded images. Increasing the number will increase the speed but temporarily reduce the responsiveness of your system in case of large drawings.
- Moved pen-related settings under ${String.fromCharCode(96)}Excalidraw appearance and behavior${String.fromCharCode(96)} to their sub-heading called ${String.fromCharCode(96)}Pen${String.fromCharCode(96)}.
- Minor error fixing and performance optimizations when loading and updating embedded images.
- Color maps in ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} may now include color keys "stroke" and "fill". If set, these will change the fill and stroke attributes of the SVG root element of the relevant file.
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
// Updates the color map of an SVG image element in the view. If a ColorMap is provided, it will be used directly.
// If an SVGColorInfo is provided, it will be converted to a ColorMap.
// The view will be marked as dirty and the image will be reset using the color map.
updateViewSVGImageColorMap(
elements: ExcalidrawImageElement | ExcalidrawImageElement[],
colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]
): Promise<void>;
// Retrieves the color map for an image element.
// The color map contains information about the mapping of colors used in the image.
// If the element already has a color map, it will be returned.
getColorMapForImageElement(el: ExcalidrawElement): ColorMap;
// Retrieves the color map for an SVG image element.
// The color map contains information about the fill and stroke colors used in the SVG.
// If the element already has a color map, it will be merged with the colors extracted from the SVG.
getColorMapForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo>;
// Extracts the fill (background) and stroke colors from an Excalidraw file and returns them as an SVGColorInfo.
getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo>;
// Extracts the fill and stroke colors from an SVG string and returns them as an SVGColorInfo.
getColorsFromSVGString(svgString: string): SVGColorInfo;
// upgraded the addImage function.
// 1. It now accepts an object as the input parameter, making your scripts more readable
// 2. AddImageOptions now includes colorMap as an optional parameter, this will only have an effect in case of SVGs and nested Excalidraws
// 3. The API function is backwards compatible, but I recommend new implementations to use the object based input
addImage(opts: AddImageOptions}): Promise<string>;
interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}
type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
interface ColorMap {
[color: string]: string;
};
${String.fromCharCode(96,96,96)}
`,
"2.7.2":`
## Fixed
- The plugin did not load on **iOS 16 and older**. [#2170](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2170)
@@ -90,56 +191,4 @@ I misread a line in the Excalidraw package code... ended up breaking image loadi
## Fixed
- Error saving when cropping images embedded from a URL (not from a file in the Vault) [#2096](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2096)
`,
"2.6.3":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/OfUWAvCgbXk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- **Cropping PDF Pages**
- Improved PDF++ cropping: You can now double-click cropped images in Excalidraw to adjust the crop area, which will also appear as a highlight in PDF++. This feature applies to PDF cut-outs created in version 2.6.3 and beyond.
- **Insert Last Active PDF Page as Image**
- New command palette action lets you insert the currently active PDF page into Excalidraw. Ideal for setups with PDF and Excalidraw side-by-side. You can assign a hotkey for quicker access. Cropped areas in Excalidraw will show as highlights in PDF++.
## Fixed
- Fixed **Close Settings** button toggle behavior [#2085](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2085)
- Resolved text wrapping issues causing layout shifts due to trailing whitespaces [#8714](https://github.com/excalidraw/excalidraw/pull/8714)
- **Aspect Ratio and Size Reset** commands now function correctly with cropped images.
- **Cropped Drawings**: Adjustments to cropped Excalidraw drawings are now supported. However, for nested Excalidraw drawings, it's recommended to use area, group, and frame references instead of cropping.
## Refactoring
- Further font loading optimizations on Excalidraw.com; no impact expected in Obsidian [#8693](https://github.com/excalidraw/excalidraw/pull/8693)
- Text wrapping improvements [#8715](https://github.com/excalidraw/excalidraw/pull/8715)
- Plugin initiation and error handling
`,
"2.6.2":`
## Fixed
- Image scaling issue with SVGs that miss the width and height property. [#8729](https://github.com/excalidraw/excalidraw/issues/8729)
`,
"2.6.1":`
## New
- Pen-mode single-finger panning enabled also for the "Selection" tool.
- You can disable pen-mode single-finger panning in Plugin Settings under Excalidraw Appearance and Behavior [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
## Fixed
- Text tool did not work in pen-mode using finger [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
- Pasting images to Excalidraw from the web resulted in filenames of "image_1.png", "image_2.png" instead of "Pasted Image TIMESTAMP" [#2081](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2081)
`,
"2.6.0":`
## Performance
- Much faster plugin initialization. Down from 1000-3000ms to 100-300ms. According to my testing speed varies on a wide spectrum depending on device, size of Vault and other plugins being loaded. I measured values ranging from 84ms up to 782ms [#2068](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2068)
- Faster loading of scenes with many embedded illustrations or PDF pages.
- SVG export results in even smaller files by further optimizing which characters are included in the embedded fonts. [#8641](https://github.com/excalidraw/excalidraw/pull/8641)
## New
- Image cropping tool. Double click the image to crop it. [#8613](https://github.com/excalidraw/excalidraw/pull/8613)
- Single finger panning in pen mode.
- Native handwritten CJK Font support [8530](https://github.com/excalidraw/excalidraw/pull/8530)
- Created a new **Fonts** section in settings. This includes configuration of the "Local Font" and downloading of the CJK fonts in case you need them offline.
- Option under **Appearance and Behavior / Link Click** to disable double-click link navigation in view mode. [#2075](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2075)
- New RU translation 🙏[@tovBender](https://github.com/tovBender)
## Updated
- CN translation 🙏[@dmscode](https://github.com/dmscode)
`,
};

View File

@@ -1,4 +1,4 @@
import { App, FuzzySuggestModal, Notice, TFile } from "obsidian";
import { App, FuzzySuggestModal, Notice } from "obsidian";
import { t } from "../../lang/helpers";
import ExcalidrawView from "src/view/ExcalidrawView";
import { getEA } from "src/core";

View File

@@ -157,6 +157,15 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
after: "",
},
{
field: "addAppendUpdateCustomData",
code: "addAppendUpdateCustomData(id: string, newData: Partial<Record<string, unknown>>)",
desc: "Add, modify keys in element customData and preserve existing keys.\n" +
"Creates customData={} if it does not exist.\n" +
"Takes the element ID for an element in the elementsDict and the new data to add or modify.\n" +
"To delete keys set key value in newData to undefined. so {keyToBeDeleted:undefined} will be deleted.",
after: "",
},
{
field: "addToGroup",
code: "addToGroup(objectIds: []): string;",
@@ -297,8 +306,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addImage",
code: "async addImage(topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink. imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\nSet scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\nanchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.",
code: "async addImage(opts: {topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean, colorMap?: ColorMap}): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink.\n"+
"imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\n"+
"Set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\n"+
"anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.\n"+
"colorMap is only used for SVG images and nested Excalidraw images. See the Shade Master script and the Deconstruct Selected Elements script for examples using colorMap.\n"+
"type ColorMap = { [color: string]: string; }",
after: "",
},
{
@@ -415,6 +429,47 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Returns the TFile file handle for the image element",
after: "",
},
{
field: "updateViewSVGImageColorMap",
code: "async updateViewSVGImageColorMap(elements: ExcalidrawImageElement | ExcalidrawImageElement[], colors: ColorMap | SVGColorInfo | ColorMap[] | SVGColorInfo[]): Promise<void>;",
desc: 'Updates the color map of an SVG image element in the view. If a ColorMap is provided, it will be used directly. If an SVGColorInfo is provided, it will be converted to a ColorMap. The view will be marked as dirty (i.e. will be saved at next scheduled time) and the image will be reset using the color map.\n'+
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>\n' +
'type ColorMap = { [color: string]: string; }',
after: "",
},
{
field: "getColorMapForImageElement",
code: "getColorMapForImageElement(el: ExcalidrawElement): ColorMap",
desc: 'Retrieves the color map for an image element. The color map contains information about the mapping of colors used in the image. If the element already has a color map, it will be returned. The colorMap does not include all colors in the image, only those that have been mapped.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type ColorMap = { [color: string]: string; }',
after: "",
},
{
field: "getSVGColorInfoForImgElement",
code: "async getColorMapForImgElement(el: ExcalidrawElement): Promise<SVGColorInfo>",
desc: 'This function must be awaited. Retrieves the color map for an SVG image element. The color map contains information about the fill and stroke colors used in the SVG. If the element already has a color map, it will be merged with the colors extracted from the SVG.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "getColosFromExcalidrawFile",
code: "async getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVGColorInfo>",
desc: 'Must be awaited. Extracts the fill (background) and stroke colors from an excalidraw file and returns them as an SVGColorInfo. The SVGColorInfo is a map where the keys are the colors used in the SVG and the values contain information about whether the color is used for fill, stroke, or both.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "getColorsFromSVGString",
code: "getColorsFromSVGString(svgString: string): SVGColorInfo",
desc: 'Extracts the fill and stroke colors from an SVG string and returns them as an SVGColorInfo. The SVGColorInfo is a map where the keys are the colors used in the SVG and the values contain information about whether the color is used for fill, stroke, or both.\n' +
'See "Shade Master" scritp in Script Library for an example of using this function.\n\n' +
'type SVGColorInfo = Map<string, { mappedTo: string; fill: boolean; stroke: boolean; }>',
after: "",
},
{
field: "copyViewElementsToEAforEditing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void;",

View File

@@ -13,7 +13,7 @@ import {
FRONTMATTER_KEYS,
getCSSFontDefinition,
} from "../constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { createSVG } from "src/utils/excalidrawAutomateUtils";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
import { ExportSettings } from "../view/ExcalidrawView";
import { t } from "../lang/helpers";
@@ -73,7 +73,8 @@ type ImgData = {
dataURL: DataURL;
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
size: Size;
pdfPageViewProps?: PDFPageViewProps;
};
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
@@ -82,8 +83,16 @@ export type FileData = BinaryFileData & {
size: Size;
hasSVGwithBitmap: boolean;
shouldScale: boolean; //true if image should maintain its area, false if image should display at 100% its size
pdfPageViewProps?: PDFPageViewProps;
};
export type PDFPageViewProps = {
left: number;
bottom: number;
right: number;
top: number;
}
export type Size = {
height: number;
width: number;
@@ -107,6 +116,20 @@ 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)) {
if(oldColor === "stroke" || oldColor === "fill") {
const [svgTag, prefix, suffix] = (svg.match(/(<svg[^>]*)(>)/i) || []) as string[];
if (!svgTag) continue;
svg = svg.replace(
svgTag,
svgTag.match(new RegExp(`${oldColor}=["'][^"']*["']`))
? prefix.replace(
new RegExp(`${oldColor}=["'][^"']*["']`,'i'),
`${oldColor}="${newColor}"`) + suffix
: `${prefix} ${oldColor}="${newColor}"${suffix}`
);
continue;
}
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi');
@@ -137,6 +160,8 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
}
}
if("fill" in colorMap) svg.setAttribute("fill", colorMap.fill);
if("stroke" in colorMap) svg.setAttribute("stroke", colorMap.stroke);
for (const child of svg.childNodes) {
childNodes(child);
}
@@ -161,6 +186,7 @@ export class EmbeddedFile {
public isLocalLink: boolean = false;
public hyperlink:DataURL;
public colorMap: ColorMap | null = null;
public pdfPageViewProps: PDFPageViewProps;
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
@@ -236,12 +262,14 @@ export class EmbeddedFile {
return this.mtime !== this.file.stat.mtime;
}
public setImage(
imgBase64: string,
mimeType: MimeType,
size: Size,
isDark: boolean,
isSVGwithBitmap: boolean,
public setImage({ imgBase64, mimeType, size, isDark, isSVGwithBitmap, pdfPageViewProps } : {
imgBase64: string;
mimeType: MimeType;
size: Size;
isDark: boolean;
isSVGwithBitmap: boolean;
pdfPageViewProps?: PDFPageViewProps;
}
) {
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
return;
@@ -250,6 +278,7 @@ export class EmbeddedFile {
this.imgInverted = this.img = "";
}
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
this.pdfPageViewProps = pdfPageViewProps;
this.size = size;
this.mimeType = mimeType;
switch (isDark && isSVGwithBitmap) {
@@ -329,6 +358,7 @@ export class EmbeddedFilesLoader {
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
pdfPageViewProps?: PDFPageViewProps;
}> {
const result = await this._getObsidianImage(inFile, depth);
this.emptyPDFDocsMap();
@@ -515,7 +545,7 @@ export class EmbeddedFilesLoader {
) {
return null;
}
const ab = isHyperLink || isPDF
const ab = isHyperLink || isPDF || isExcalidrawFile
? null
: isLocalLink
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
@@ -536,9 +566,9 @@ export class EmbeddedFilesLoader {
const excalidrawSVG = isExcalidrawFile ? dURL : null;
const [pdfDataURL, pdfSize] = isPDF
const [pdfDataURL, pdfSize, pdfPageViewProps] = isPDF
? await this.pdfToDataURL(file,linkParts)
: [null, null];
: [null, null, null];
let mimeType: MimeType = isPDF
? "image/png"
@@ -577,26 +607,33 @@ export class EmbeddedFilesLoader {
return {
mimeType,
fileId: await generateIdFromFile(
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
isHyperLink || isPDF || isExcalidrawFile ? (new TextEncoder()).encode(dataURL as string) : ab,
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
),
dataURL,
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
hasSVGwithBitmap,
size,
pdfPageViewProps,
};
} catch(e) {
return null;
}
}
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number,
isThemeChange:boolean = false,
fileIDWhiteList?: Set<FileId>,
) {
public async loadSceneFiles({
excalidrawData,
addFiles,
depth,
isThemeChange = false,
fileIDWhiteList,
}: {
excalidrawData: ExcalidrawData;
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void;
depth: number;
isThemeChange?: boolean;
fileIDWhiteList?: Set<FileId>;
}) {
if(depth > 7) {
new Notice(t("INFINITE_LOOP_WARNING")+depth.toString(), 6000);
@@ -612,7 +649,7 @@ export class EmbeddedFilesLoader {
files.push([]);
let batch = 0;
function* loadIterator():Generator<Promise<void>> {
function* loadIterator(this: EmbeddedFilesLoader):Generator<Promise<void>> {
while (!(entry = entries.next()).done) {
if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue;
const embeddedFile: EmbeddedFile = entry.value[1];
@@ -632,20 +669,22 @@ export class EmbeddedFilesLoader {
created: data.created,
size: data.size,
hasSVGwithBitmap: data.hasSVGwithBitmap,
shouldScale: embeddedFile.shouldScale()
shouldScale: embeddedFile.shouldScale(),
pdfPageViewProps: data.pdfPageViewProps,
};
files[batch].push(fileData);
}
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
//this will reload the image in light/dark mode when switching themes
const fileData = {
const fileData: FileData = {
mimeType: embeddedFile.mimeType,
id: id,
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
created: embeddedFile.mtime,
size: embeddedFile.size,
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
shouldScale: embeddedFile.shouldScale()
shouldScale: embeddedFile.shouldScale(),
pdfPageViewProps: embeddedFile.pdfPageViewProps,
};
files[batch].push(fileData);
}
@@ -759,13 +798,14 @@ export class EmbeddedFilesLoader {
}, 1200);
const iterator = loadIterator.bind(this)();
const concurency = 3;
const concurency = this.plugin.settings.renderingConcurrency;
await new PromisePool(iterator, concurency).all();
clearInterval(addFilesTimer);
this.emptyPDFDocsMap();
if (this.terminate) {
addFiles(undefined, this.isDark, true);
return;
}
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"});
@@ -780,7 +820,7 @@ export class EmbeddedFilesLoader {
private async pdfToDataURL(
file: TFile,
linkParts: LinkParts,
): Promise<[DataURL,{width:number, height:number}]> {
): Promise<[DataURL,Size, PDFPageViewProps]> {
try {
let width = 0, height = 0;
const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file);
@@ -791,6 +831,7 @@ export class EmbeddedFilesLoader {
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));
let viewProps: PDFPageViewProps;
// Render the page
const renderPage = async (num:number) => {
@@ -801,8 +842,8 @@ export class EmbeddedFilesLoader {
const page = await pdfDoc.getPage(num);
// Set scale
const viewport = page.getViewport({ scale });
height = canvas.height = viewport.height;
width = canvas.width = viewport.width;
height = canvas.height = Math.round(viewport.height);
width = canvas.width = Math.round(viewport.width);
const renderCtx = {
canvasContext: ctx,
@@ -823,9 +864,10 @@ export class EmbeddedFilesLoader {
continue;
}
}
const [left, bottom, right, top] = page.view;
viewProps = {left, bottom, right, top};
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
@@ -845,19 +887,19 @@ export class EmbeddedFilesLoader {
const canvas = await renderPage(pageNum);
if(canvas) {
const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
const result: [DataURL,Size, PDFPageViewProps] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
canvas.toBlob(async (blob) => {
const dataURL = await blobToBase64(blob);
resolve(dataURL);
});
})}` as DataURL, {width, height}];
})}` as DataURL, {width, height}, viewProps];
canvas.width = 0; //free memory iOS bug
canvas.height = 0;
return result;
}
} catch(e) {
console.log(e);
return [null,null];
return [null, null, null];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -649,7 +649,6 @@ export class ExcalidrawData {
containers.forEach((container: any) => {
if(ellipseAndRhombusContainerWrapping && !container.customData?.legacyTextWrap) {
addAppendUpdateCustomData(container, {legacyTextWrap: true});
//container.customData = {...container.customData, legacyTextWrap: true};
}
const filteredBoundElements = container.boundElements.filter(
(boundEl: any) => elements.some((el: any) => el.id === boundEl.id),
@@ -1569,13 +1568,13 @@ export class ExcalidrawData {
filepath,
);
embeddedFile.setImage(
dataURL,
embeddedFile.setImage({
imgBase64: dataURL,
mimeType,
{ height: 0, width: 0 },
scene.appState?.theme === "dark",
mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
);
size: { height: 0, width: 0 },
isDark: scene.appState?.theme === "dark",
isSVGwithBitmap: mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
});
this.setFile(key as FileId, embeddedFile);
return file;
}
@@ -1593,7 +1592,9 @@ export class ExcalidrawData {
const pageRef = ef.linkParts.original.split("#")?.[1];
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
const link = ef.linkParts.original +
getPDFRect({elCrop: el.crop, scale: pdfScale, customData: el.customData}) +
(restOfLink ? restOfLink : "]]");
el.link = `[[${link}`;
this.elementLinks.set(el.id, el.link);
dirty = true;
@@ -1992,7 +1993,7 @@ export class ExcalidrawData {
isLocalLink: data.isLocalLink,
path: data.hyperlink,
blockrefData: null,
hasSVGwithBitmap: data.isSVGwithBitmap
hasSVGwithBitmap: data.isSVGwithBitmap,
});
return;
}
@@ -2028,7 +2029,8 @@ export class ExcalidrawData {
this.file.path,
masterFile.blockrefData
? masterFile.path + "#" + masterFile.blockrefData
: masterFile.path
: masterFile.path,
masterFile.colorMapJSON
);
this.files.set(fileId,embeddedFile);
return embeddedFile;

View File

@@ -253,6 +253,12 @@ export class ScriptEngine {
if (!view || !script || !title) {
return;
}
//addresses the situation when after paste text element IDs are not updated to 8 characters
//linked to onPaste save issue with the false parameter
if(view.getScene().elements.some(el=>!el.isDeleted && el.type === "text" && el.id.length > 8)) {
await view.save(false, true);
}
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
this.eaInstances.push(ea);

View File

@@ -0,0 +1,34 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { TFile } from "obsidian";
import { FileId } from "src/core";
import { ColorMap, MimeType, PDFPageViewProps, Size } from "src/shared/EmbeddedFileLoader";
export type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
export type ImageInfo = {
mimeType: MimeType,
id: FileId,
dataURL: DataURL,
created: number,
isHyperLink?: boolean,
hyperlink?: string,
file?:string | TFile,
hasSVGwithBitmap: boolean,
latex?: string,
size?: Size,
colorMap?: ColorMap,
pdfPageViewProps?: PDFPageViewProps,
}
export interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}

View File

@@ -1,4 +1,4 @@
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
import { Notice, RequestUrlResponse } from "obsidian";
import ExcalidrawPlugin from "src/core/main";
type MessageContent =

View File

@@ -1,26 +1,29 @@
//for future use, not used currently
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { PDFPageViewProps } from "src/shared/EmbeddedFileLoader";
export function getPDFCropRect (props: {
scale: number,
link: string,
naturalHeight: number,
naturalWidth: number,
pdfPageViewProps: PDFPageViewProps,
}) : ImageCrop | null {
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
if (!rectVal || rectVal.length !== 5) {
return null;
}
const { left, bottom } = props.pdfPageViewProps;
const R0 = parseInt(rectVal[1]);
const R1 = parseInt(rectVal[2]);
const R2 = parseInt(rectVal[3]);
const R3 = parseInt(rectVal[4]);
return {
x: R0 * props.scale,
y: (props.naturalHeight/props.scale - R3) * props.scale,
x: (R0 - left) * props.scale,
y: (bottom + props.naturalHeight/props.scale - R3) * props.scale,
width: (R2 - R0) * props.scale,
height: (R3 - R1) * props.scale,
naturalWidth: props.naturalWidth,
@@ -28,10 +31,16 @@ export function getPDFCropRect (props: {
}
}
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
const R0 = elCrop.x / scale;
export function getPDFRect({elCrop, scale, customData}:{
elCrop: ImageCrop, scale: number, customData: Record<string, unknown>
}): string {
const { left, bottom } = (customData && customData.pdfPageViewProps)
? customData.pdfPageViewProps as PDFPageViewProps
: { left: 0, bottom: 0 };
const R0 = elCrop.x / scale + left;
const R2 = elCrop.width / scale + R0;
const R3 = (elCrop.naturalHeight - elCrop.y) / scale;
const R3 = bottom + (elCrop.naturalHeight - elCrop.y) / scale;
const R1 = R3 - elCrop.height / scale;
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
}

View File

@@ -3,7 +3,7 @@ import { ColorMaster } from "@zsviczian/colormaster";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import ExcalidrawView from "src/view/ExcalidrawView";
import { DynamicStyle } from "src/types/types";
import { cloneElement } from "src/shared/ExcalidrawAutomate";
import { cloneElement } from "./excalidrawAutomateUtils";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { addAppendUpdateCustomData } from "./utils";
import { mutateElement } from "src/constants/constants";

View File

@@ -0,0 +1,820 @@
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import ExcalidrawPlugin from "src/core/main";
import {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { normalizePath, TFile } from "obsidian";
import ExcalidrawView, { ExportSettings, getTextMode } from "src/view/ExcalidrawView";
import {
GITHUB_RELEASES,
getCommonBoundingBox,
restore,
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
EXCALIDRAW_PLUGIN,
getFontFamilyString,
getLineHeight,
measureText,
} from "src/constants/constants";
import {
//debug,
errorlog,
getEmbeddedFilenameParts,
getLinkParts,
getPNG,
getSVG,
isVersionNewerThanOther,
scaleLoadedImage,
} from "src/utils/utils";
import { GenericInputPrompt, NewFileActions } from "src/shared/Dialogs/Prompt";
import { t } from "src/lang/helpers";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import {
postOpenAI as _postOpenAI,
extractCodeBlocks as _extractCodeBlocks,
} from "../utils/AIUtils";
import { ColorMap, EmbeddedFilesLoader, FileData } from "src/shared/EmbeddedFileLoader";
import { SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, REGEX_LINK } from "src/shared/ExcalidrawData";
import { getFrameBasedOnFrameNameOrId } from "./excalidrawViewUtils";
import { ScriptEngine } from "src/shared/Scripts";
declare const PLUGIN_VERSION:string;
export function isSVGColorInfo(obj: ColorMap | SVGColorInfo): boolean {
return (
typeof obj === 'object' &&
obj !== null &&
'stroke' in obj &&
'fill' in obj &&
'mappedTo' in obj
);
}
export function mergeColorMapIntoSVGColorInfo(
colorMap: ColorMap,
svgColorInfo: SVGColorInfo
): SVGColorInfo {
if(colorMap) {
for(const key of Object.keys(colorMap)) {
if(svgColorInfo.has(key)) {
svgColorInfo.get(key).mappedTo = colorMap[key];
}
}
}
return svgColorInfo;
}
export function svgColorInfoToColorMap(svgColorInfo: SVGColorInfo): ColorMap {
const colorMap: ColorMap = {};
svgColorInfo.forEach((info, color) => {
if (info.fill || info.stroke) {
colorMap[color] = info.mappedTo;
}
});
return colorMap;
}
//Remove identical key-value pairs from a ColorMap
export function filterColorMap(colorMap: ColorMap): ColorMap {
return Object.fromEntries(
Object.entries(colorMap).filter(([key, value]) => key.toLocaleLowerCase() !== value?.toLocaleLowerCase())
);
}
export function updateOrAddSVGColorInfo(
svgColorInfo: SVGColorInfo,
color: string,
info: {fill?: boolean, stroke?: boolean, mappedTo?: string}
): SVGColorInfo {
const {fill, stroke, mappedTo} = info;
color = color.toLocaleLowerCase();
const colorData = svgColorInfo.get(color) || {mappedTo: color, fill: false, stroke: false};
if(fill !== undefined) {
colorData.fill = fill;
}
if(stroke !== undefined) {
colorData.stroke = stroke;
}
if(mappedTo !== undefined) {
colorData.mappedTo = mappedTo;
}
return svgColorInfo.set(color, colorData);
}
export function getEmbeddedFileForImageElment(ea: ExcalidrawAutomate, el: ExcalidrawElement) {
if (!ea.targetView || !ea.targetView?._loaded) {
errorMessage("targetView not set", "getViewFileForImageElement()");
return null;
}
if (!el || el.type !== "image") {
errorMessage(
"Must provide an image element as input",
"getViewFileForImageElement()",
);
return null;
}
return ea.targetView?.excalidrawData?.getFile(el.fileId);
}
export function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({
where: "ExcalidrawAutomate",
source,
message:
"targetView not set, or no longer active. Use setView before calling this function",
});
break;
case "mobile not supported":
errorlog({
where: "ExcalidrawAutomate",
source,
message: "this function is not available on Obsidian Mobile",
});
break;
default:
errorlog({
where: "ExcalidrawAutomate",
source,
message: message??"unknown error",
});
}
}
export function isColorStringTransparent(color: string): boolean {
const rgbaHslaTransparentRegex = /^(rgba|hsla)\(.*?,.*?,.*?,\s*0(\.0+)?\)$/i;
const hexTransparentRegex = /^#[a-fA-F0-9]{8}00$/i;
return rgbaHslaTransparentRegex.test(color) || hexTransparentRegex.test(color);
}
export function initExcalidrawAutomate(
plugin: ExcalidrawPlugin,
): ExcalidrawAutomate {
const ea = new ExcalidrawAutomate(plugin);
//@ts-ignore
window.ExcalidrawAutomate = ea;
return ea;
}
export function normalizeLinePoints(
points: [x: number, y: number][],
//box: { x: number; y: number; w: number; h: number },
): number[][] {
const p = [];
const [x, y] = points[0];
for (let i = 0; i < points.length; i++) {
p.push([points[i][0] - x, points[i][1] - y]);
}
return p;
}
export function getLineBox(
points: [x: number, y: number][]
):{x:number, y:number, w: number, h:number} {
const [x1, y1, x2, y2] = estimateLineBound(points);
return {
x: x1,
y: y1,
w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]),
h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1])
};
}
export function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
export function _measureText(
newText: string,
fontSize: number,
fontFamily: number,
lineHeight: number,
): {w: number, h:number} {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
fontSize = 20;
}
if (!fontFamily) {
fontFamily = 1;
lineHeight = getLineHeight(fontFamily);
}
const metrics = measureText(
newText,
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
lineHeight
);
return { w: metrics.width, h: metrics.height };
}
export async function getTemplate(
plugin: ExcalidrawPlugin,
fileWithPath: string,
loadFiles: boolean = false,
loader: EmbeddedFilesLoader,
depth: number,
convertMarkdownLinksToObsidianURLs: boolean = false,
): Promise<{
elements: any;
appState: any;
frontmatter: string;
files: any;
hasSVGwithBitmap: boolean;
plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
}> {
const app = plugin.app;
const vault = app.vault;
const filenameParts = getEmbeddedFilenameParts(fileWithPath);
const templatePath = normalizePath(filenameParts.filepath);
const file = app.metadataCache.getFirstLinkpathDest(templatePath, "");
let hasSVGwithBitmap = false;
if (file && file instanceof TFile) {
const data = (await vault.read(file))
.replaceAll("\r\n", "\n")
.replaceAll("\r", "\n");
const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin);
if (file.extension === "excalidraw") {
await excalidrawData.loadLegacyData(data, file);
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
elements: excalidrawData.scene.elements,
hostFile: file,
}) : excalidrawData.scene.elements,
appState: excalidrawData.scene.appState,
frontmatter: "",
files: excalidrawData.scene.files,
hasSVGwithBitmap,
plaintext: "",
};
}
const textMode = getTextMode(data);
await excalidrawData.loadData(
data,
file,
textMode,
);
let trimLocation = data.search(/^##? Text Elements$/m);
if (trimLocation == -1) {
trimLocation = data.search(/##? Drawing\n/);
}
let scene = excalidrawData.scene;
let groupElements:ExcalidrawElement[] = scene.elements;
if(filenameParts.hasGroupref) {
const el = filenameParts.hasSectionref
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length > 0) {
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
if(el) {
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
}
}
if(filenameParts.hasTaskbone) {
groupElements = groupElements.filter( el =>
el.type==="freedraw" ||
( el.type==="image" &&
!plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file)
));
}
let fileIDWhiteList:Set<FileId>;
if(groupElements.length < scene.elements.length) {
fileIDWhiteList = new Set<FileId>();
groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId));
}
if (loadFiles) {
//debug({where:"getTemplate",template:file.name,loader:loader.uid});
await loader.loadSceneFiles({
excalidrawData,
addFiles: (fileArray: FileData[]) => {
//, isDark: boolean) => {
if (!fileArray || fileArray.length === 0) {
return;
}
for (const f of fileArray) {
if (f.hasSVGwithBitmap) {
hasSVGwithBitmap = true;
}
excalidrawData.scene.files[f.id] = {
mimeType: f.mimeType,
id: f.id,
dataURL: f.dataURL,
created: f.created,
};
}
scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
},
depth,
fileIDWhiteList
});
}
excalidrawData.destroy();
const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
let files:any = {};
const sceneFilesSize = Object.values(scene.files).length;
if (sceneFilesSize > 0) {
if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) {
Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => {
files[f.id] = f;
});
} else {
files = scene.files;
}
}
const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
return {
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
elements: groupElements,
hostFile: file,
}) : groupElements,
appState: scene.appState,
frontmatter,
plaintext: frontmatter !== filehead
? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
: "",
files,
hasSVGwithBitmap,
};
}
return {
elements: [],
appState: {},
frontmatter: null,
files: [],
hasSVGwithBitmap,
plaintext: "",
};
}
export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#E7E7E7" /><text x="${width / 2}" y="${height / 2}" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="${Math.min(width, height) / 5}" fill="#888">Placeholder</text></svg>`;
return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
};
export async function createPNG(
templatePath: string = undefined,
scale: number = 1,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
forceTheme: string = undefined,
canvasTheme: string = undefined,
canvasBackgroundColor: string = undefined,
automateElements: ExcalidrawElement[] = [],
plugin: ExcalidrawPlugin,
depth: number,
padding?: number,
imagesDict?: any,
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
padding = padding ?? plugin.settings.exportPaddingSVG;
const template = templatePath
? await getTemplate(plugin, templatePath, true, loader, depth)
: null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
const files = imagesDict ?? {};
if(template?.files) {
Object.values(template.files).forEach((f:any)=>{
if(!f.dataURL.startsWith("http")) {
files[f.id]=f;
};
});
}
return await getPNG(
{
type: "excalidraw",
version: 2,
source: GITHUB_RELEASES+PLUGIN_VERSION,
elements,
appState: {
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
{
withBackground:
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
isMask: exportSettings?.isMask ?? false,
},
padding,
scale,
);
}
export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
elements: ExcalidrawElement[];
hostFile: TFile;
}): ExcalidrawElement[] => {
return elements.map((el)=>{
if(el.link && el.link.startsWith("[")) {
const partsArray = REGEX_LINK.getResList(el.link)[0];
if(!partsArray?.value) return el;
let linkText = REGEX_LINK.getLink(partsArray);
if (linkText.search("#") > -1) {
const linkParts = getLinkParts(linkText, hostFile);
linkText = linkParts.path;
}
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
return el;
}
const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
linkText,
hostFile.path,
);
if(!file) {
return el;
}
let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
originalLink: el.link,
obsidianLink: link,
linkedFile: file,
hostFile: hostFile
});
}
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
newElement.link = link;
return newElement;
}
return el;
})
}
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
});
}
export async function createSVG(
templatePath: string = undefined,
embedFont: boolean = false,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
forceTheme: string = undefined,
canvasTheme: string = undefined,
canvasBackgroundColor: string = undefined,
automateElements: ExcalidrawElement[] = [],
plugin: ExcalidrawPlugin,
depth: number,
padding?: number,
imagesDict?: any,
convertMarkdownLinksToObsidianURLs: boolean = false,
): Promise<SVGSVGElement> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
if(typeof exportSettings.skipInliningFonts === "undefined") {
exportSettings.skipInliningFonts = !embedFont;
}
const template = templatePath
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
: null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
padding = padding ?? plugin.settings.exportPaddingSVG;
const files = imagesDict ?? {};
if(template?.files) {
Object.values(template.files).forEach((f:any)=>{
files[f.id]=f;
});
}
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
const filenameParts = getEmbeddedFilenameParts(templatePath);
const svg = await getSVG(
{
//createAndOpenDrawing
type: "excalidraw",
version: 2,
source: GITHUB_RELEASES+PLUGIN_VERSION,
elements,
appState: {
theme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
},
files,
},
{
withBackground:
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme,
isMask: exportSettings?.isMask ?? false,
...filenameParts?.hasClippedFrameref
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
: {},
},
padding,
null,
);
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
if(
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
(filenameParts.hasBlockref || filenameParts.hasSectionref)
) {
let el = filenameParts.hasSectionref
? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true)
: elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
if(el.length>0) {
const containerId = el[0].containerId;
if(containerId) {
el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId));
}
const elBB = plugin.ea.getBoundingBox(el);
const drawingBB = plugin.ea.getBoundingBox(elements);
svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX;
svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY;
const width = elBB.width + 2*padding;
svg.viewBox.baseVal.width = width;
const height = elBB.height + 2*padding;
svg.viewBox.baseVal.height = height;
svg.setAttribute("width", `${width}`);
svg.setAttribute("height", `${height}`);
}
}
if (template?.hasSVGwithBitmap) {
svg.setAttribute("hasbitmap", "true");
}
return svg;
}
function estimateLineBound(points: any): [number, number, number, number] {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
return [minX, minY, maxX, maxY];
}
export function estimateBounds(
elements: ExcalidrawElement[],
): [number, number, number, number] {
const bb = getCommonBoundingBox(elements);
return [bb.minX, bb.minY, bb.maxX, bb.maxY];
}
export function repositionElementsToCursor(
elements: ExcalidrawElement[],
newPosition: { x: number; y: number },
center: boolean = false,
): ExcalidrawElement[] {
const [x1, y1, x2, y2] = estimateBounds(elements);
let [offsetX, offsetY] = [0, 0];
if (center) {
[offsetX, offsetY] = [
newPosition.x - (x1 + x2) / 2,
newPosition.y - (y1 + y2) / 2,
];
} else {
[offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1];
}
elements.forEach((element: any) => {
//using any so I can write read-only propery x & y
element.x = element.x + offsetX;
element.y = element.y + offsetY;
});
return restore({elements}, null, null).elements;
}
export const insertLaTeXToView = (view: ExcalidrawView) => {
const app = view.plugin.app;
const ea = view.plugin.ea;
GenericInputPrompt.Prompt(
view,
view.plugin,
app,
t("ENTER_LATEX"),
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
view.plugin.settings.latexBoilerplate,
undefined,
3
).then(async (formula: string) => {
if (!formula) {
return;
}
ea.reset();
await ea.addLaTex(0, 0, formula);
ea.setView(view);
ea.addElementsToView(true, false, true);
});
};
export const search = async (view: ExcalidrawView) => {
const ea = view.plugin.ea;
ea.reset();
ea.setView(view);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
if (elements.length === 0) {
return;
}
let text = await ScriptEngine.inputPrompt(
view,
view.plugin,
view.plugin.app,
"Search for",
"use quotation marks for exact match",
"",
);
if (!text) {
return;
}
const res = text.matchAll(/"(.*?)"/g);
let query: string[] = [];
let parts;
while (!(parts = res.next()).done) {
query.push(parts.value[1]);
}
text = text.replaceAll(/"(.*?)"/g, "");
query = query.concat(text.split(" ").filter((s:string) => s.length !== 0));
ea.targetView.selectElementsMatchingQuery(elements, query);
};
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getTextElementsMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.type === "text" &&
query.some((q) => {
if (exactMatch) {
const text = el.rawText.toLowerCase().split("\n")[0].trim();
const m = text.match(/^#*(# .*)/);
if (!m || m.length !== 2) {
return false;
}
return m[1] === q.toLowerCase();
}
const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getFrameElementsMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.type === "frame" &&
query.some((q) => {
if (exactMatch) {
const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
const m = text.match(/^#*(# .*)/);
if (!m || m.length !== 2) {
return false;
}
return m[1] === q.toLowerCase();
}
const text = el.name
? el.name.toLowerCase().replaceAll("\n", " ").trim()
: "";
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getElementsWithLinkMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: any) =>
el.link &&
query.some((q) => {
const text = el.link.toLowerCase().trim();
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getImagesMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
excalidrawData: ExcalidrawData,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: ExcalidrawElement) =>
el.type === "image" &&
query.some((q) => {
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
const text = filename ?? equation;
if(!text) return false;
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
export const cloneElement = (el: ExcalidrawElement):any => {
const newEl = JSON.parse(JSON.stringify(el));
newEl.version = el.version + 1;
newEl.updated = Date.now();
newEl.versionNonce = Math.floor(Math.random() * 1000000000);
return newEl;
}
export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
}
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.length
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
: null;
};

View File

@@ -4,7 +4,7 @@ import { App, Modal, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "../shared/ExcalidrawData";
import ExcalidrawView from "src/view/ExcalidrawView";
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./utils";
import { cleanSectionHeading } from "./obsidianUtils";
import { getEA } from "src/core";
@@ -12,6 +12,8 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
import { nanoid } from "nanoid";
import { t } from "src/lang/helpers";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { EmbeddedFile } from "src/shared/EmbeddedFileLoader";
export async function insertImageToView(
ea: ExcalidrawAutomate,
@@ -431,4 +433,35 @@ export function displayFontMessage(app: App) {
}
modal.open();
}
export async function toggleImageAnchoring(
el: ExcalidrawImageElement,
view: ExcalidrawView,
shouldAnchor: boolean,
ef: EmbeddedFile,
) {
const ea = getEA(view) as ExcalidrawAutomate;
let imgEl = view.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as Mutable<ExcalidrawImageElement>;
if(!imgEl) {
ea.destroy();
return;
}
ea.copyViewElementsToEAforEditing([imgEl]);
imgEl = ea.getElements()[0] as Mutable<ExcalidrawImageElement>;
if(!imgEl.customData) {
imgEl.customData = {};
}
imgEl.customData.isAnchored = shouldAnchor;
if(shouldAnchor) {
const {height, width} = ef.size;
const dX = width - imgEl.width;
const dY = height - imgEl.height;
imgEl.height = height;
imgEl.width = width;
imgEl.x -= dX/2;
imgEl.y -= dY/2;
}
await ea.addElementsToView(false, false);
ea.destroy();
}

View File

@@ -2,7 +2,7 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../shared/ExcalidrawData";
import ExcalidrawView, { TextMode } from "src/view/ExcalidrawView";
import { rotatedDimensions } from "./utils";
import { getBoundTextElementId } from "src/shared/ExcalidrawAutomate";
import { getBoundTextElementId } from "src/utils/excalidrawAutomateUtils";
export const getElementsAtPointer = (
pointer: any,

38
src/utils/sliderUtils.ts Normal file
View File

@@ -0,0 +1,38 @@
import { Setting } from "obsidian";
export type SliderSetting = {
name: string;
desc?: string | DocumentFragment;
min: number;
max: number;
step: number;
value: number;
minWidth?: string;
onChange: (value: number) => void;
}
export const createSliderWithText = (
container: HTMLElement,
settings: SliderSetting
): void => {
let valueText: HTMLDivElement;
new Setting(container)
.setName(settings.name)
.setDesc(settings.desc || '')
.addSlider((slider) =>
slider
.setLimits(settings.min, settings.max, settings.step)
.setValue(settings.value)
.onChange(async (value) => {
valueText.innerText = ` ${value.toString()}`;
settings.onChange(value);
}),
)
.settingEl.createDiv("", (el) => {
valueText = el;
el.style.minWidth = settings.minWidth || '2.3em';
el.style.textAlign = "right";
el.innerText = ` ${settings.value.toString()}`;
});
}

View File

@@ -18,20 +18,21 @@ import {
getContainerElement,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExportSettings } from "../view/ExcalidrawView";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./fileUtils";
import { generateEmbeddableLink } from "./customEmbeddableUtils";
import { FILENAMEPARTS } from "../types/utilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./obsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/shared/ExcalidrawAutomate";
import { updateElementLinksToObsidianLinks } from "./excalidrawAutomateUtils";
import { CropImage } from "../shared/CropImage";
import opentype from 'opentype.js';
import { runCompressionWorker } from "src/shared/Workers/compression-worker";
import Pool from "es6-promise-pool";
import { FileData } from "../shared/EmbeddedFileLoader";
import { t } from "src/lang/helpers";
import ExcalidrawScene from "src/shared/svgToExcalidraw/elements/ExcalidrawScene";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -415,11 +416,17 @@ export async function getImageSize (
});
};
export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement {
export function addAppendUpdateCustomData (
el: Mutable<ExcalidrawElement>,
newData: Partial<Record<string, unknown>>
): ExcalidrawElement {
if(!newData) return el;
if(!el.customData) el.customData = {};
for (const key in newData) {
if(typeof newData[key] === "undefined") continue;
if(typeof newData[key] === "undefined") {
delete el.customData[key];
continue;
}
el.customData[key] = newData[key];
}
return el;
@@ -447,7 +454,7 @@ export function scaleLoadedImage (
scene.elements
.filter((e: any) => e.type === "image" && e.fileId === img.id)
.forEach((el: any) => {
.forEach((el: Mutable<ExcalidrawImageElement>) => {
const [elWidth, elHeight] = [el.width, el.height];
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
const elCrop: ImageCrop = el.crop;

View File

@@ -57,16 +57,16 @@ import {
getContainerElement,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
import {
repositionElementsToCursor,
ExcalidrawAutomate,
getTextElementsMatchingQuery,
cloneElement,
getFrameElementsMatchingQuery,
getElementsWithLinkMatchingQuery,
getImagesMatchingQuery,
getBoundTextElementId
} from "../shared/ExcalidrawAutomate";
} from "../utils/excalidrawAutomateUtils";
import { t } from "../lang/helpers";
import {
ExcalidrawData,
@@ -105,8 +105,9 @@ import {
shouldEmbedScene,
_getContainerElement,
arrayToMap,
addAppendUpdateCustomData,
} from "../utils/utils";
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
import { splitFolderAndFilename } from "../utils/fileUtils";
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "../shared/Dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -128,7 +129,7 @@ import { getEA } from "src/core"
import { anyModifierKeysPressed, emulateKeysForLinkClick, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "../utils/modifierkeyHelper";
import { setDynamicStyle } from "../utils/dynamicStyling";
import { CustomEmbeddable, renderWebView } from "./components/CustomEmbeddable";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "../utils/excalidrawViewUtils";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup, toggleImageAnchoring } from "../utils/excalidrawViewUtils";
import { imageCache } from "../shared/ImageCache";
import { CanvasNodeFactory, ObsidianCanvasNode } from "./managers/CanvasNodeFactory";
import { EmbeddableMenu } from "./components/menu/EmbeddableActionsMenu";
@@ -147,6 +148,7 @@ import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
import { getPDFCropRect } from "../utils/PDFUtils";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
import { ImageInfo } from "src/types/excalidrawAutomateTypes";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -223,18 +225,26 @@ export const addFiles = async (
.filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
.forEach((f:FileData) => {
s.scene.elements
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && (
(el.crop && el.crop?.naturalWidth !== f.size.width) || !el.customData?.pdfPageViewProps
))
.forEach((el:Mutable<ExcalidrawImageElement>) => {
s.dirty = true;
const scale = f.size.width / el.crop.naturalWidth;
el.crop = {
x: el.crop.x * scale,
y: el.crop.y * scale,
width: el.crop.width * scale,
height: el.crop.height * scale,
naturalWidth: f.size.width,
naturalHeight: f.size.height,
};
if(el.crop) {
s.dirty = true;
const scale = f.size.width / el.crop.naturalWidth;
el.crop = {
x: el.crop.x * scale,
y: el.crop.y * scale,
width: el.crop.width * scale,
height: el.crop.height * scale,
naturalWidth: f.size.width,
naturalHeight: f.size.height,
};
}
if(!el.customData?.pdfPageViewProps) {
s.dirty = true;
addAppendUpdateCustomData(el, { pdfPageViewProps: f.pdfPageViewProps});
}
});
});
@@ -250,13 +260,14 @@ export const addFiles = async (
if (view.excalidrawData.hasFile(f.id)) {
const embeddedFile = view.excalidrawData.getFile(f.id);
embeddedFile.setImage(
f.dataURL,
f.mimeType,
f.size,
embeddedFile.setImage({
imgBase64: f.dataURL,
mimeType: f.mimeType,
size: f.size,
isDark,
f.hasSVGwithBitmap,
);
isSVGwithBitmap: f.hasSVGwithBitmap,
pdfPageViewProps: f.pdfPageViewProps,
});
}
if (view.excalidrawData.hasEquation(f.id)) {
const latex = view.excalidrawData.getEquation(f.id).latex;
@@ -973,9 +984,14 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (!link || ef.linkParts.original === link) {
return;
}
const originalAnchor = Boolean(ef.linkParts.original.endsWith("|100%"));
const nextAnchor = Boolean(link.endsWith("|100%"));
ef.resetImage(this.file.path, link);
this.excalidrawData.setFile(fileId, ef);
this.setDirty(2);
if(originalAnchor !== nextAnchor) {
await toggleImageAnchoring(el, this, nextAnchor, ef)
}
await this.save(false);
await sleep(100);
if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) {
@@ -1082,7 +1098,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
isFullscreen(): boolean {
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen");
return Boolean(document.body.querySelector(".excalidraw-hidden"));
return Boolean(this.ownerDocument.body.querySelector(".excalidraw-hidden"));
}
exitFullscreen() {
@@ -2418,7 +2434,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
public activeLoader: EmbeddedFilesLoader = null;
private nextLoader: EmbeddedFilesLoader = null;
public async loadSceneFiles(isThemeChange: boolean = false) {
public async loadSceneFiles(isThemeChange: boolean = false, fileIDWhiteList?: Set<FileId>, callback?: Function) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSceneFiles, "ExcalidrawView.loadSceneFiles", isThemeChange);
if (!this.excalidrawAPI) {
return;
@@ -2428,13 +2444,18 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const runLoader = (l: EmbeddedFilesLoader) => {
this.nextLoader = null;
this.activeLoader = l;
l.loadSceneFiles(
this.excalidrawData,
(files: FileData[], isDark: boolean, final:boolean = true) => {
if (!files) {
return;
}
addFiles(files, this, isDark);
l.loadSceneFiles({
excalidrawData: this.excalidrawData,
addFiles: (files: FileData[], isDark: boolean, final:boolean = true) => {
if(callback && final) {
callback();
}
if(!this.file || !this.excalidrawAPI) {
return; //The view was closed in the mean time
}
if (files && files.length > 0) {
addFiles(files, this, isDark);
}
if(!final) return;
this.activeLoader = null;
if (this.nextLoader) {
@@ -2454,8 +2475,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return false;
})
}
},0,isThemeChange,
);
},
depth: 0,
isThemeChange,
fileIDWhiteList,
});
};
if (!this.activeLoader) {
runLoader(loader);
@@ -2484,7 +2508,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return;
}
this.semaphores.saving = true;
let reloadFiles = false;
let reloadFiles = new Set<FileId>();
try {
const deletedIds = inData.deletedElements.map(el=>el.id);
@@ -2507,13 +2531,13 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
incomingElement.fileId,
inData.getFile(incomingElement.fileId)
);
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
} else if (inData.getEquation(incomingElement.fileId)) {
this.excalidrawData.setEquation(
incomingElement.fileId,
inData.getEquation(incomingElement.fileId)
)
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
}
break;
}
@@ -2592,7 +2616,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
incomingElement.fileId,
inData.getFile(incomingElement.fileId)
);
reloadFiles = true;
reloadFiles.add(incomingElement.fileId);
}
}
})
@@ -2603,7 +2627,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.setDirty(3);
}
this.updateScene({elements: sceneElements, storeAction: "capture"});
if(reloadFiles) this.loadSceneFiles();
if(reloadFiles.size>0) {
this.loadSceneFiles(false,reloadFiles);
}
} catch(e) {
errorlog({
where:"ExcalidrawView.synchronizeWithData",
@@ -3307,19 +3333,31 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.x<this.currentPosition.x || top.y>this.currentPosition.y || bottom.y<this.currentPosition.y;
const id = ea.addText(this.currentPosition.x, this.currentPosition.y, text);
await this.addElements(ea.getElements(), isPointerOutsideVisibleArea, save, undefined, true);
await this.addElements({
newElements: ea.getElements(),
repositionToCursor: isPointerOutsideVisibleArea,
save: save,
newElementsOnTop: true
});
ea.destroy();
return id;
};
public async addElements(
newElements: ExcalidrawElement[],
repositionToCursor: boolean = false,
save: boolean = false,
images: any,
newElementsOnTop: boolean = false,
shouldRestoreElements: boolean = false,
): Promise<boolean> {
public async addElements({
newElements,
repositionToCursor = false,
save = false,
images,
newElementsOnTop = false,
shouldRestoreElements = false,
}: {
newElements: ExcalidrawElement[];
repositionToCursor?: boolean;
save?: boolean;
images?: {[key: FileId]: ImageInfo};
newElementsOnTop?: boolean;
shouldRestoreElements?: boolean;
}): Promise<boolean> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements);
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!api) {
@@ -3376,40 +3414,38 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
? el.concat(newElements.filter((e) => !removeList.includes(e.id)))
: newElements.filter((e) => !removeList.includes(e.id)).concat(el);
this.updateScene(
{
elements,
storeAction: "capture",
},
shouldRestoreElements,
);
if (images && Object.keys(images).length >0) {
const files: BinaryFileData[] = [];
Object.keys(images).forEach((k) => {
const files: BinaryFileData[] = [];
if (images && Object.keys(images).length >0) {
Object.keys(images).forEach((k: FileId) => {
files.push({
mimeType: images[k].mimeType,
id: images[k].id,
dataURL: images[k].dataURL,
created: images[k].created,
});
if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) {
if (images[k].file || images[k].isHyperLink) { //|| images[k].isLocalLink but isLocalLink was never passed
const embeddedFile = new EmbeddedFile(
this.plugin,
this.file.path,
images[k].isHyperLink && !images[k].isLocalLink
images[k].isHyperLink //&& !images[k].isLocalLink local link is never passed to addElements
? images[k].hyperlink
: images[k].file,
: (typeof images[k].file === "string" ? images[k].file : images[k].file.path),
);
const st: AppState = api.getAppState();
embeddedFile.setImage(
images[k].dataURL,
images[k].mimeType,
images[k].size,
st.theme === "dark",
images[k].hasSVGwithBitmap,
);
embeddedFile.setImage({
imgBase64: images[k].dataURL,
mimeType: images[k].mimeType,
size: images[k].size,
isDark: st.theme === "dark",
isSVGwithBitmap: images[k].hasSVGwithBitmap,
pdfPageViewProps: images[k].pdfPageViewProps,
});
this.excalidrawData.setFile(images[k].id, embeddedFile);
if(images[k].pdfPageViewProps) {
elements.filter((e) => e.type === "image" && e.fileId === images[k].id).forEach((e) => {
addAppendUpdateCustomData(e, {pdfPageViewProps: images[k].pdfPageViewProps});
});
}
}
if (images[k].latex) {
this.excalidrawData.setEquation(images[k].id, {
@@ -3418,8 +3454,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
});
}
});
}
this.updateScene(
{
elements,
storeAction: "capture",
},
shouldRestoreElements,
);
if(files.length > 0) {
api.addFiles(files);
}
api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer));
if (save) {
await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
@@ -3978,7 +4026,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
link,
naturalHeight: fd.size.height,
naturalWidth: fd.size.width,
pdfPageViewProps: fd.pdfPageViewProps,
});
addAppendUpdateCustomData(el, {pdfPageViewProps: fd.pdfPageViewProps});
if(el.crop) {
el.width = el.crop.width/this.plugin.settings.pdfScale;
el.height = el.crop.height/this.plugin.settings.pdfScale;

View File

@@ -4,7 +4,7 @@ import * as React from "react";
import { ActionButton } from "./ActionButton";
import { ICONS, saveIcon, stringToSVG } from "../../../constants/actionIcons";
import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../../../constants/constants";
import { insertLaTeXToView, search } from "../../../shared/ExcalidrawAutomate";
import { insertLaTeXToView, search } from "../../../utils/excalidrawAutomateUtils";
import ExcalidrawView, { TextMode } from "../../ExcalidrawView";
import { t } from "../../../lang/helpers";
import { ReleaseNotes } from "../../../shared/Dialogs/ReleaseNotes";

View File

@@ -6,7 +6,7 @@
.excalidraw-wrapper {
height: 100%;
margin: 0px;
background-color: white;
background-color: var(--background-primary);
position:relative;
}
@@ -664,4 +664,4 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw-setting-desc:hover {
background-color: var(--background-modifier-hover);
color: var(--text-accent);
}
}