Compare commits

..

27 Commits

Author SHA1 Message Date
zsviczian f5475bfde6 imagepath hook 2025-01-20 22:40:27 +01:00
zsviczian 5171978c37 Merge pull request #2217 from zsviczian/PDF-fix
CodeQL / Analyze (javascript) (push) Has been cancelled
Pdf crop fix
2025-01-17 06:46:59 +01:00
zsviczian ea4a0c91e8 added PDF samples for testing 2025-01-17 06:45:30 +01:00
zsviczian 34af6dd447 getPDFCropRect, getPDFRect improved 90,180,270 calc, still issue with page offsets 2025-01-15 23:22:35 +01:00
zsviczian ed2e700946 Merge pull request #2215 from zsviczian/local-graph-embed-sync
CodeQL / Analyze (javascript) (push) Has been cancelled
Set local graph when embeddable is activated/deactivated #2200
2025-01-15 09:38:13 +01:00
zsviczian 7eb23ab5e1 Set local graph when embeddable is activated/deactivated #2200 2025-01-15 08:22:38 +00:00
zsviczian 7cccf1d4e2 2.7.6-beta-1
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-14 22:57:14 +01:00
zsviczian 2a5545964c update package-lock.json, update packages, style.css to support new arrow picker 2025-01-14 22:15:28 +01:00
zsviczian 4ce22883cc Merge pull request #2214 from zsviczian/allow-new-image-formats
CodeQL / Analyze (javascript) (push) Waiting to run
fix: allow jfif and avif
2025-01-14 16:01:01 +01:00
zsviczian 272804afc8 allow jfif and avif 2025-01-14 14:59:47 +00:00
zsviczian dc0b50f717 Update How-to.yml
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
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-05 23:09:16 +01:00
zsviczian 63c56e0e98 similar elements allows selection of containers
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
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
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
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
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-31 08:37:46 +01:00
51 changed files with 13365 additions and 1592 deletions
+9
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:
+10 -1
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:
-1
View File
@@ -4,7 +4,6 @@
# npm
node_modules
package-lock.json
# build
main.js
+1 -1
View File
@@ -1 +1 @@
18
18
+1340
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -13,11 +13,12 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-typescript": "^12.1.2",
"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.7.3"
}
}
}
+3 -3
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)
};
};
+26
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",
]
}
-4
View File
@@ -1,4 +0,0 @@
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:
+686 -340
View File
File diff suppressed because it is too large Load Diff
+69 -13
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();
+32 -7
View File
@@ -7,16 +7,41 @@ 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();
+68
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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.7.6-beta-1",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.7.5",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
+9290
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -24,7 +24,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-24",
"@zsviczian/excalidraw": "0.17.6-26",
"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",
"tslib": "^2.6.1",
"rollup-plugin-typescript2": "^0.36.0",
"tslib": "^2.8.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2",
"typescript": "^5.7.3",
"fs-extra": "^11.2.0",
"uglify-js": "^3.19.3"
},
+35
View File
@@ -105,6 +105,41 @@
*/
//ea.onFileCreateHook = (data) => {};
/**
* If set, this callback is triggered when a image is being saved in Excalidraw.
* You can use this callback to customize the naming and path of pasted images to avoid
* default names like "Pasted image 123147170.png" being saved in the attachments folder,
* and instead use more meaningful names based on the Excalidraw file or other criteria,
* plus save the image in a different folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the excalidraw generated name and default path.
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* The currentImageName is the name of the image generated by excalidraw or provided during paste.
*
* @param data - An object containing the following properties:
* @property {string} [currentImageName] - Default name for the image.
* @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath } = data;
* const ext = currentImageName.split('.').pop();
* // Generate a new filepath based on the drawing file name and other criteria
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
* onImageFilePathHook: (data: {
* currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
* drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
* }) => string = null;
*/
//ea.onImageFileNameHook = (data) => {};
/**
* If set, this callback is triggered whenever the active canvas color changes
* onCanvasColorChangeHook: (
+1 -1
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;
File diff suppressed because one or more lines are too long
+12 -2
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
+2 -5
View File
@@ -29,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 {
+1 -1
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";
+35 -5
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);
}
+26
View File
@@ -17,6 +17,32 @@ 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
+1 -1
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";
+9
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;",
+93 -34
View File
@@ -13,13 +13,13 @@ 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";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "../core/main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "../utils/fileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "../utils/fileUtils";
import {
errorlog,
getDataURL,
@@ -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,17 @@ 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;
rotate?: number; //may be undefined in legacy files
}
export type Size = {
height: number;
width: number;
@@ -177,6 +187,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;
@@ -252,12 +263,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;
@@ -266,6 +279,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) {
@@ -345,6 +359,7 @@ export class EmbeddedFilesLoader {
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
pdfPageViewProps?: PDFPageViewProps;
}> {
const result = await this._getObsidianImage(inFile, depth);
this.emptyPDFDocsMap();
@@ -552,9 +567,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"
@@ -600,6 +615,7 @@ export class EmbeddedFilesLoader {
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
hasSVGwithBitmap,
size,
pdfPageViewProps,
};
} catch(e) {
return null;
@@ -634,7 +650,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];
@@ -654,20 +670,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);
}
@@ -803,7 +821,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);
@@ -814,6 +832,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) => {
@@ -824,8 +843,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,
@@ -846,20 +865,60 @@ export class EmbeddedFilesLoader {
continue;
}
}
if(validRect) {
const [left, bottom, _, top] = page.view;
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const [left, bottom, right, top] = page.view;
viewProps = {left, bottom, right, top};
viewProps.rotate = page.rotate;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
if(validRect) {
const pageHeight = top - bottom;
const pageWidth = right - left;
if(!page.rotate || page.rotate === 0) {
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 90) {
width = (cropRect[3] - cropRect[1]) * scale;
height = (cropRect[2] - cropRect[0]) * scale;
const crop = {
left: cropRect[1] * scale,
top: (pageHeight - cropRect[2]) * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 180) {
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = {
left: (pageWidth - cropRect[2]) * scale,
top: cropRect[1] * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 270) {
width = (cropRect[3] - cropRect[1]) * scale;
height = (cropRect[2] - cropRect[0]) * scale;
const crop = {
left: (pageWidth - cropRect[3]) * scale,
top: cropRect[0] * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
}
@@ -868,19 +927,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
+29 -18
View File
@@ -20,7 +20,7 @@ import {
loadSceneFonts,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { TextMode } from "../view/ExcalidrawView";
import ExcalidrawView, { TextMode } from "../view/ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
@@ -52,7 +52,7 @@ import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "..
import { DEBUGGING, debug } from "../utils/debugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils";
import { getNewUniqueFilepath } from "../utils/fileUtils";
import { getNewUniqueFilepath, splitFolderAndFilename } from "../utils/fileUtils";
import { t } from "../lang/helpers";
import { displayFontMessage } from "../utils/excalidrawViewUtils";
import { getPDFRect } from "../utils/PDFUtils";
@@ -480,7 +480,7 @@ export class ExcalidrawData {
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
constructor(
private plugin: ExcalidrawPlugin,
private plugin: ExcalidrawPlugin, private view?: ExcalidrawView,
) {
this.app = this.plugin.app;
this.files = new Map<FileId, EmbeddedFile>();
@@ -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),
@@ -1547,13 +1546,23 @@ export class ExcalidrawData {
}
}
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
const filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
let hookFilepath:string;
const ea = this.view?.getHookServer();
if(ea?.onImageFilePathHook) {
hookFilepath = ea.onImageFilePathHook({
currentImageName: fname,
drawingFilePath: this.view?.file?.path,
})
}
/*
const filepath = (
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname)
).filepath;*/
let filepath:string;
if(hookFilepath) {
const {folderpath, filename} = splitFolderAndFilename(hookFilepath);
filepath = getNewUniqueFilepath(this.app.vault,filename,folderpath);
} else {
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
}
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
if(!arrayBuffer) return null;
@@ -1569,13 +1578,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 +1602,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 +2003,7 @@ export class ExcalidrawData {
isLocalLink: data.isLocalLink,
path: data.hyperlink,
blockrefData: null,
hasSVGwithBitmap: data.isSVGwithBitmap
hasSVGwithBitmap: data.isSVGwithBitmap,
});
return;
}
+3 -2
View File
@@ -1,7 +1,7 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { TFile } from "obsidian";
import { FileId } from "src/core";
import { ColorMap, MimeType } from "src/shared/EmbeddedFileLoader";
import { ColorMap, MimeType, PDFPageViewProps, Size } from "src/shared/EmbeddedFileLoader";
export type SVGColorInfo = Map<string, {
mappedTo: string;
@@ -19,8 +19,9 @@ export type ImageInfo = {
file?:string | TFile,
hasSVGwithBitmap: boolean,
latex?: string,
size?: {height: number, width: number},
size?: Size,
colorMap?: ColorMap,
pdfPageViewProps?: PDFPageViewProps,
}
export interface AddImageOptions {
+1 -1
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 =
+102 -11
View File
@@ -1,37 +1,128 @@
//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 rotate = props.pdfPageViewProps.rotate ?? 0;
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]);
if(rotate === 90) {
const _top = R0;
const _left = R1;
const _bottom = R2;
const _right = R3;
const x = _left * props.scale;
const y = _top * props.scale;
return {
x,
y,
width: _right*props.scale - x,
height: _bottom*props.scale - y,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
if(rotate === 180) {
const _right = R0;
const _top = R1;
const _left = R2;
const _bottom = R3;
const y = _top * props.scale;
const x = props.naturalWidth - _left * props.scale;
return {
x,
y,
width: props.naturalWidth - x - _right * props.scale,
height: _bottom * props.scale - y,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
if(rotate === 270) {
const _bottom = R0;
const _right = R1;
const _top = R2;
const _left = R3;
const x = props.naturalWidth - _left * props.scale;
const y = props.naturalHeight - _top * props.scale;
return {
x,
y,
width: props.naturalWidth - x - _right * props.scale,
height: props.naturalHeight - y - _bottom * props.scale,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
// default to 0° rotation
const _left = R0;
const _bottom = R1;
const _right = R2;
const _top = R3;
return {
x: R0 * props.scale,
y: (props.naturalHeight/props.scale - R3) * props.scale,
width: (R2 - R0) * props.scale,
height: (R3 - R1) * props.scale,
x: (_left - left) * props.scale,
y: props.naturalHeight - (_top - bottom) * props.scale,
width: (_right - _left) * props.scale,
height: (_top - _bottom) * props.scale,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
const R0 = elCrop.x / scale;
const R2 = elCrop.width / scale + R0;
const R3 = (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)}`;
}
export function getPDFRect({elCrop, scale, customData}:{
elCrop: ImageCrop, scale: number, customData: Record<string, unknown>
}): string {
const rotate = (customData.pdfPageViewProps as PDFPageViewProps)?.rotate ?? 0;
const { left, bottom } = (customData && customData.pdfPageViewProps)
? customData.pdfPageViewProps as PDFPageViewProps
: { left: 0, bottom: 0 };
if(rotate === 90) {
const _top = (elCrop.y) / scale;
const _left = (elCrop.x) / scale;
const _bottom = (elCrop.height + elCrop.y) / scale;
const _right = (elCrop.width + elCrop.x) / scale;
return `&rect=${Math.round(_top)},${Math.round(_left)},${Math.round(_bottom)},${Math.round(_right)}`;
}
if(rotate === 180) {
const _right = (elCrop.naturalWidth-elCrop.x-elCrop.width) / scale;
const _top = (elCrop.y) / scale;
const _left = (elCrop.naturalWidth - elCrop.x) / scale;
const _bottom = (elCrop.height + elCrop.y) / scale;
return `&rect=${Math.round(_right)},${Math.round(_top)},${Math.round(_left)},${Math.round(_bottom)}`;
}
if(rotate === 270) {
const _bottom = (elCrop.naturalHeight - elCrop.height-elCrop.y) / scale;
const _right = (elCrop.naturalWidth - elCrop.width - elCrop.x) / scale;
const _top = (elCrop.naturalHeight - elCrop.y) / scale;
const _left = (elCrop.naturalWidth - elCrop.x) / scale;
return `&rect=${Math.round(_bottom)},${Math.round(_right)},${Math.round(_top)},${Math.round(_left)}`;
}
const _left = elCrop.x / scale + left;
const _right = elCrop.width / scale + _left;
const _top = bottom + (elCrop.naturalHeight - elCrop.y) / scale;
const _bottom = _top - elCrop.height / scale;
return `&rect=${Math.round(_left)},${Math.round(_bottom)},${Math.round(_right)},${Math.round(_top)}`;
}
+12 -1
View File
@@ -1,7 +1,7 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { getParentOfClass } from "./obsidianUtils";
import { TFile, WorkspaceLeaf } from "obsidian";
import { App, TFile, WorkspaceLeaf } from "obsidian";
import { getLinkParts } from "./utils";
import ExcalidrawView from "src/view/ExcalidrawView";
@@ -55,4 +55,15 @@ export const generateEmbeddableLink = (src: string, theme: "light" | "dark"):str
}
}*/
return src;
}
export function setFileToLocalGraph(app: App, file: TFile) {
let lgv;
app.workspace.iterateAllLeaves((l) => {
if (l.view?.getViewType() === "localgraph") lgv = l.view;
});
if (lgv) {
//@ts-ignore
lgv.loadFile(file);
}
}
+1 -1
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";
+709 -3
View File
@@ -1,8 +1,49 @@
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { errorlog } from "./utils";
import { ColorMap } from "src/shared/EmbeddedFileLoader";
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 (
@@ -112,3 +153,668 @@ export function isColorStringTransparent(color: string): boolean {
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;
};
+1 -1
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,
+12 -5
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;
+87 -52
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";
@@ -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;
@@ -357,7 +368,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
super(leaf);
this._plugin = plugin;
this.excalidrawData = new ExcalidrawData(plugin);
this.excalidrawData = new ExcalidrawData(plugin, this);
this.canvasNodeFactory = new CanvasNodeFactory(this);
this.setHookServer();
this.dropManager = new DropManager(this);
@@ -1087,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() {
@@ -3322,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) {
@@ -3391,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, {
@@ -3433,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
@@ -3993,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;
+14 -1
View File
@@ -6,7 +6,7 @@ import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDa
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "src/constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ObsidianCanvasNode } from "src/view/managers/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "src/utils/customEmbeddableUtils";
import { processLinkText, patchMobileView, setFileToLocalGraph } from "src/utils/customEmbeddableUtils";
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
declare module "obsidian" {
@@ -154,6 +154,15 @@ function RenderObsidianView(
}; //cleanup on unmount
}, [isActiveRef.current, containerRef.current]);
//set local graph to view when deactivating embeddables
React.useEffect(() => {
if(file === view.file) {
return;
}
if(!isActiveRef.current) {
setFileToLocalGraph(view.app, view.file);
}
}, [isActiveRef.current]);
//--------------------------------------------------------------------------------
//Mount the workspace leaf or the canvas node depending on subpath
@@ -408,6 +417,10 @@ function RenderObsidianView(
return;
}
if(file !== view.file) {
setFileToLocalGraph(view.app, file);
}
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
//Handle markdown leaf
//@ts-ignore
+1 -1
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";
+7
View File
@@ -361,9 +361,16 @@ div.excalidraw-draginfo {
}
.excalidraw [data-radix-popper-content-wrapper] {
/*Overrides position:fixed in popover*/
position: absolute !important;
}
.excalidraw .Island .App-mobile-menu,
.excalidraw .Island.App-menu__left {
/*Arrow Picker Popover Overflow*/
overflow: visible;
}
.excalidraw__embeddable-container .view-header {
display: none !important;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.