mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
clippedframe, framesettings
This commit is contained in:
@@ -377,6 +377,7 @@ export class EmbeddedFilesLoader {
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
hasSectionref: false,
|
||||
blockref: null,
|
||||
sectionref: null,
|
||||
@@ -399,12 +400,15 @@ export class EmbeddedFilesLoader {
|
||||
await createSVG(
|
||||
hasFilenameParts
|
||||
? (filenameParts.hasGroupref || filenameParts.hasBlockref ||
|
||||
filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
filenameParts.hasSectionref || filenameParts.hasFrameref ||
|
||||
filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path)
|
||||
: file?.path,
|
||||
false, //false
|
||||
exportSettings,
|
||||
hasFilenameParts && filenameParts.hasClippedFrameref
|
||||
? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
@@ -568,7 +572,8 @@ export class EmbeddedFilesLoader {
|
||||
return {
|
||||
mimeType,
|
||||
fileId: await generateIdFromFile(
|
||||
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab,
|
||||
inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined
|
||||
),
|
||||
dataURL,
|
||||
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
|
||||
@@ -1017,7 +1022,7 @@ const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Pro
|
||||
return svgToBase64(svg) as DataURL;
|
||||
};
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
|
||||
@@ -1032,4 +1037,37 @@ export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> =>
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
// Convert the file ArrayBuffer to a Uint8Array
|
||||
const fileArray = new Uint8Array(file);
|
||||
|
||||
// If a key is provided, concatenate it to the file data
|
||||
let dataToHash: Uint8Array;
|
||||
if (key) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyArray = encoder.encode(key);
|
||||
dataToHash = new Uint8Array(fileArray.length + keyArray.length);
|
||||
dataToHash.set(fileArray);
|
||||
dataToHash.set(keyArray, fileArray.length);
|
||||
} else {
|
||||
dataToHash = fileArray;
|
||||
}
|
||||
|
||||
// Hash the combined data (file and key, if provided)
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash);
|
||||
id =
|
||||
// Convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// Convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
@@ -2416,13 +2416,19 @@ export class ExcalidrawAutomate {
|
||||
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param frameElement - the frame element for which to get the elements
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @param shouldIncludeFrame - if true, the frame element will be included in the returned array
|
||||
* this is useful when generating an image in which you want the frame to be clipped
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
getElementsInFrame(
|
||||
frameElement: ExcalidrawElement,
|
||||
elements: ExcalidrawElement[],
|
||||
shouldIncludeFrame: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
if(!frameElement || !elements || frameElement.type !== "frame") return [];
|
||||
return elements.filter(el=>el.frameId === frameElement.id);
|
||||
return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2879,15 +2885,14 @@ async function getTemplate(
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref) {
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements)
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
el.type==="freedraw" ||
|
||||
@@ -2965,6 +2970,7 @@ export async function createPNG(
|
||||
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -3060,6 +3066,7 @@ export async function createSVG(
|
||||
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
|
||||
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
const svg = await getSVG(
|
||||
{
|
||||
//createAndOpenDrawing
|
||||
@@ -3071,6 +3078,7 @@ export async function createSVG(
|
||||
theme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
@@ -3079,6 +3087,9 @@ export async function createSVG(
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
...filenameParts?.hasClippedFrameref
|
||||
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: {},
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
@@ -3086,9 +3097,8 @@ export async function createSVG(
|
||||
|
||||
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
if(
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
|
||||
@@ -174,6 +174,12 @@ export interface ExportSettings {
|
||||
withBackground: boolean;
|
||||
withTheme: boolean;
|
||||
isMask: boolean;
|
||||
frameRendering?: { //optional, overrides relevant appState settings for rendering the frame
|
||||
enabled: boolean;
|
||||
name: boolean;
|
||||
outline: boolean;
|
||||
clip: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const HIDE = "excalidraw-hidden";
|
||||
@@ -4811,6 +4817,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
onClose
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
contextMenuActions.push([
|
||||
renderContextMenuAction(
|
||||
React,
|
||||
t("COPY_DRAWING_LINK"),
|
||||
() => {
|
||||
navigator.clipboard.writeText(`![[${this.file.path}]]`);
|
||||
},
|
||||
onClose
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if(this.getViewSelectedElements().filter(el=>el.type==="embeddable").length === 1) {
|
||||
@@ -5684,11 +5701,19 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let buttons = [];
|
||||
if(isFrame) {
|
||||
switch(prefix) {
|
||||
case "clippedframe=":
|
||||
buttons = [
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
];
|
||||
break;
|
||||
case "area=":
|
||||
case "group=":
|
||||
case "frame=":
|
||||
buttons = [
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
];
|
||||
break;
|
||||
@@ -5696,6 +5721,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5727,7 +5753,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const alias = await ScriptEngine.inputPrompt(
|
||||
this,
|
||||
this.plugin,
|
||||
app,
|
||||
this.app,
|
||||
"Set link alias",
|
||||
"Leave empty if you do not want to set an alias",
|
||||
"",
|
||||
|
||||
@@ -105,11 +105,13 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
|
||||
const png =
|
||||
quickPNG ??
|
||||
(await createPNG(
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref)
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
scale,
|
||||
exportSettings,
|
||||
filenameParts.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -184,11 +186,13 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
|
||||
|
||||
let svg = convertSVGStringToElement((
|
||||
await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
filenameParts?.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -229,11 +233,13 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
|
||||
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
|
||||
? maybeSVG
|
||||
: convertSVGStringToElement((await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
false,
|
||||
exportSettings,
|
||||
filenameParts.hasClippedFrameref
|
||||
? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}}
|
||||
: exportSettings,
|
||||
loader,
|
||||
theme,
|
||||
null,
|
||||
@@ -571,7 +577,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
}
|
||||
|
||||
@@ -661,7 +667,7 @@ const tmpObsidianWYSIWYG = async (
|
||||
} else {
|
||||
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
|
||||
if(warningEl) {
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=)[^ ]*)/)?.[1];
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
|
||||
if(ref) {
|
||||
attr.fname = file.path + ref;
|
||||
areaPreview = true;
|
||||
|
||||
File diff suppressed because one or more lines are too long
82
src/dialogs/FrameSettings.ts
Normal file
82
src/dialogs/FrameSettings.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export const showFrameSettings = (ea: ExcalidrawAutomate) => {
|
||||
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
|
||||
|
||||
// Create modal dialog
|
||||
const frameSettingsModal = new ea.obsidian.Modal(app);
|
||||
|
||||
frameSettingsModal.onOpen = () => {
|
||||
const {contentEl} = frameSettingsModal;
|
||||
|
||||
contentEl.createEl("h1", {text: t("FRAME_SETTINGS_TITLE")});
|
||||
|
||||
const settings = { enabled, clip, name, outline };
|
||||
|
||||
// Add toggles
|
||||
const enableFramesSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_ENABLE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.enabled)
|
||||
.onChange(value => {
|
||||
settings.enabled = value;
|
||||
hideComponent(displayFrameNameSetting, !value);
|
||||
hideComponent(displayFrameOutlineSetting, !value);
|
||||
hideComponent(enableFrameClippingSetting, !value);
|
||||
})
|
||||
);
|
||||
|
||||
const displayFrameNameSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTIGNS_NAME"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.name)
|
||||
.onChange(value => settings.name = value)
|
||||
);
|
||||
|
||||
const displayFrameOutlineSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_OUTLINE"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.outline)
|
||||
.onChange(value => settings.outline = value)
|
||||
);
|
||||
|
||||
const enableFrameClippingSetting = new ea.obsidian.Setting(contentEl)
|
||||
.setName(t("FRAME_SETTINGS_CLIP"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings.clip)
|
||||
.onChange(value => settings.clip = value)
|
||||
);
|
||||
|
||||
// Hide or show components based on initial state
|
||||
hideComponent(displayFrameNameSetting, !settings.enabled);
|
||||
hideComponent(displayFrameOutlineSetting, !settings.enabled);
|
||||
hideComponent(enableFrameClippingSetting, !settings.enabled);
|
||||
|
||||
// Add OK button
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText("OK")
|
||||
.onClick(() => {
|
||||
// Update appState with new settings
|
||||
ea.viewUpdateScene({
|
||||
// @ts-ignore
|
||||
appState: {
|
||||
frameRendering: settings
|
||||
}
|
||||
});
|
||||
frameSettingsModal.close();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
frameSettingsModal.onClose = () => {
|
||||
ea.destroy();
|
||||
}
|
||||
frameSettingsModal.open();
|
||||
};
|
||||
|
||||
// Function to hide or show a component
|
||||
function hideComponent(comp:any, value:any) {
|
||||
comp.settingEl.style.display = value ? "none" : "";
|
||||
}
|
||||
@@ -17,6 +17,15 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.2.10": `
|
||||
## New
|
||||
- Area, Group, Frame, and Clipped-Frame references to images now also work when pasting images to Excalidraw.
|
||||
- New reference type ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} works in the same way as ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} but will display the elements clipped by the frame. ${String.fromCharCode(96)}clippedframe=${String.fromCharCode(96)} will always display the image with zero padding.
|
||||
- New command palette action: ${String.fromCharCode(96)}Frame Settings${String.fromCharCode(96)} gives you fine-grained control over how frames are rendered. Frame settings will also be reflected in image exports. For example, if you hide the frame name or outline, then in exports they will not be visible.
|
||||
|
||||
## Fixed
|
||||
- Frame title font in exports.
|
||||
`,
|
||||
"2.2.9": `
|
||||
## New
|
||||
- Improved the "Open the back-of-the-note of the selected Excalidraw image" action. It now works with grouped elements and keeps the popout window within the visible screen area when elements are close to the top of the canvas. Note: Due to an Obsidian bug, I do not recommend using this feature with versions 1.6.0 - 1.6.6, if you have Obsidian Sync enabled, because Obsidian may freeze when closing the popout window. It functions properly in Obsidian versions before 1.6.0 and from 1.6.7 onwards.
|
||||
|
||||
@@ -481,6 +481,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
|
||||
after: ""
|
||||
},
|
||||
{
|
||||
field: "getElementsInFrame",
|
||||
code: " getElementsInFrame(frameElement: ExcalidrawElement,elements: ExcalidrawElement[],shouldIncludeFrame: boolean = false,): ExcalidrawElement[];",
|
||||
desc: "Gets all the elements from elements[] that are inside the frameElement. If shouldIncludeFrame is true, the frameElement will also be included in the result.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "activeScript",
|
||||
code: "activeScript: string;",
|
||||
|
||||
@@ -50,17 +50,20 @@ export default {
|
||||
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
|
||||
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
|
||||
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
|
||||
COPY_ELEMENT_LINK: "Copy markdown link for selected element(s)",
|
||||
COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)",
|
||||
COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
`Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
|
||||
INSERT_LINK_TO_ELEMENT_GROUP:
|
||||
"Copy 'group=' markdown link for selected element to clipboard.",
|
||||
"Copy 'group=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"Copy 'area=' markdown link for selected element to clipboard.",
|
||||
"Copy 'area=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"Copy 'frame=' markdown link for selected element to clipboard.",
|
||||
"Copy 'frame=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED:
|
||||
"Copy 'clippedframe=' ![[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"Copy markdown link for selected element to clipboard.",
|
||||
"Copy [[link]] for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
@@ -136,6 +139,7 @@ export default {
|
||||
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
|
||||
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
|
||||
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
|
||||
RECURSIVE_INSERT_ERROR: "You may not recursively insert part of an image into the same image as it would create an infinite loop",
|
||||
CONVERT_TO_MARKDOWN: "Convert to file...",
|
||||
SELECT_TEXTELEMENT_ONLY: "Select text element only (not container)",
|
||||
REMOVE_LINK: "Remove text element link",
|
||||
@@ -571,7 +575,7 @@ FILENAME_HEAD: "Filename",
|
||||
"If turned off, the exported image will be transparent.",
|
||||
EXPORT_PADDING_NAME: "Image Padding",
|
||||
EXPORT_PADDING_DESC:
|
||||
"The padding (in pixels) around the exported SVG or PNG image. " +
|
||||
"The padding (in pixels) around the exported SVG or PNG image. Padding is set to 0 for clippedFrame references." +
|
||||
"If you have curved lines close to the edge of the image they might get cropped during image export. You can increase this value to avoid cropping. " +
|
||||
"You can also override this setting at a file level by adding the <code>excalidraw-export-padding: 5<code> frontmatter key.",
|
||||
EXPORT_THEME_NAME: "Export image with theme",
|
||||
@@ -824,4 +828,11 @@ FILENAME_HEAD: "Filename",
|
||||
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
|
||||
PANE_TARGET: "Link click behavior",
|
||||
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
|
||||
|
||||
//FrameSettings.ts
|
||||
FRAME_SETTINGS_TITLE: "Frame Settings",
|
||||
FRAME_SETTINGS_ENABLE: "Enable Frames",
|
||||
FRAME_SETTIGNS_NAME: "Display Frame Name",
|
||||
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
|
||||
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
|
||||
};
|
||||
|
||||
56
src/main.ts
56
src/main.ts
@@ -137,6 +137,7 @@ import { carveOutImage, carveOutPDF, createImageCropperFile } from "./utils/Carv
|
||||
import { ExcalidrawConfig } from "./utils/ExcalidrawConfig";
|
||||
import { EditorHandler } from "./CodeMirrorExtension/EditorHandler";
|
||||
import { clearMathJaxVariables } from "./LaTeX";
|
||||
import { showFrameSettings } from "./dialogs/FrameSettings";
|
||||
|
||||
declare let EXCALIDRAW_PACKAGES:string;
|
||||
declare let react:any;
|
||||
@@ -1370,6 +1371,42 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "frame-settings",
|
||||
name: t("FRAME_SETTINGS_TITLE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
showFrameSettings(getEA(view));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "copy-link-to-drawing",
|
||||
name: t("COPY_DRAWING_LINK"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
navigator.clipboard.writeText(`![[${view.file.path}]]`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "disable-frameclipping",
|
||||
name: t("TOGGLE_FRAME_CLIPPING"),
|
||||
@@ -1579,7 +1616,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
name: t("INSERT_LINK_TO_ELEMENT_FRAME"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
@@ -1590,6 +1627,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-frame-clipped",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_FRAME_CLIPPED"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.copyLinkToSelectedElementToClipboard("clippedframe=");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-area",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_AREA"),
|
||||
@@ -1640,6 +1693,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
id: "flip-image",
|
||||
name: t("FLIP_IMAGE"),
|
||||
checkCallback: (checking:boolean) => {
|
||||
if (!DEVICE.isDesktop) return;
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if(!view) return false;
|
||||
if(!view.excalidrawAPI) return false;
|
||||
|
||||
@@ -2137,7 +2137,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
d.addOption("Assistant", "Assistant");
|
||||
this.app.vault
|
||||
.getFiles()
|
||||
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))
|
||||
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
|
||||
.forEach((f: TFile) => {
|
||||
d.addOption(f.path, f.name);
|
||||
});
|
||||
|
||||
@@ -366,6 +366,10 @@ export function isTextImageTransclusion (
|
||||
if(match?.value?.[0]) {
|
||||
const link = match.value[1] ?? match.value[2];
|
||||
const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path);
|
||||
if(view.file === file) {
|
||||
new Notice(t("RECURSIVE_INSERT_ERROR"));
|
||||
return false;
|
||||
}
|
||||
if(file && file instanceof TFile) {
|
||||
if (file.extension !== "md" || view.plugin.isExcalidrawFile(file)) {
|
||||
callback(link, file);
|
||||
|
||||
@@ -25,7 +25,7 @@ export type ImageKey = {
|
||||
|
||||
const getKey = (key: ImageKey): string =>
|
||||
`${key.filepath}#${key.blockref??""}#${key.sectionref??""}#${key.isDark ? 1 : 0}#${
|
||||
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasSectionref}#${
|
||||
key.hasGroupref}#${key.hasArearef}#${key.hasFrameref}#${key.hasClippedFrameref}#${key.hasSectionref}#${
|
||||
key.previewImageType === PreviewImageType.SVGIMG
|
||||
? 1
|
||||
: key.previewImageType === PreviewImageType.PNG
|
||||
@@ -172,7 +172,7 @@ class ImageCache {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if(cursor) {
|
||||
const key = cursor.key as string;
|
||||
const isLegacyKey = key.replaceAll(/[^#]/g,"").length < 9; // introduced hasGroupref, etc. in 1.9.28
|
||||
const isLegacyKey = key.split("#").length-1 < 11; // introduced hasGroupref, etc. in 1.9.28 // introduced hasClippedFrameref in 2.2.10
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
|
||||
@@ -5,6 +5,7 @@ export type FILENAMEPARTS = {
|
||||
hasTaskbone: boolean,
|
||||
hasArearef: boolean,
|
||||
hasFrameref: boolean,
|
||||
hasClippedFrameref: boolean,
|
||||
hasSectionref: boolean,
|
||||
blockref: string,
|
||||
sectionref: string,
|
||||
|
||||
@@ -195,7 +195,35 @@ export async function getFontDataURL (
|
||||
const f = app.metadataCache.getFirstLinkpathDest(fontFileName, sourcePath);
|
||||
if (f) {
|
||||
const ab = await app.vault.readBinary(f);
|
||||
const mimeType = f.extension.startsWith("woff")
|
||||
let mimeType = "";
|
||||
let format = "";
|
||||
|
||||
switch (f.extension) {
|
||||
case "woff":
|
||||
mimeType = "application/font-woff";
|
||||
format = "woff";
|
||||
break;
|
||||
case "woff2":
|
||||
mimeType = "font/woff2";
|
||||
format = "woff2";
|
||||
break;
|
||||
case "ttf":
|
||||
mimeType = "font/ttf";
|
||||
format = "truetype";
|
||||
break;
|
||||
case "otf":
|
||||
mimeType = "font/otf";
|
||||
format = "opentype";
|
||||
break;
|
||||
default:
|
||||
mimeType = "application/octet-stream"; // Fallback if file type is unexpected
|
||||
}
|
||||
fontName = name ?? f.basename;
|
||||
dataURL = await getDataURL(ab, mimeType);
|
||||
const split = dataURL.split(";base64,", 2);
|
||||
dataURL = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${format}")}`;
|
||||
/* const mimeType = f.extension.startsWith("woff")
|
||||
? "application/font-woff"
|
||||
: "font/truetype";
|
||||
fontName = name ?? f.basename;
|
||||
@@ -203,7 +231,7 @@ export async function getFontDataURL (
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
|
||||
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
|
||||
const split = fontDef.split(";base64,", 2);
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
|
||||
}
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
@@ -273,14 +301,17 @@ export async function getSVG (
|
||||
svg = await exportToSvg({
|
||||
elements: elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
|
||||
appState: {
|
||||
...scene.appState,
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme !== "light"
|
||||
: false,
|
||||
...scene.appState,
|
||||
...exportSettings.frameRendering
|
||||
? {frameRendering: exportSettings.frameRendering}
|
||||
: {},
|
||||
},
|
||||
files: scene.files,
|
||||
exportPadding: padding,
|
||||
exportPadding: exportSettings.frameRendering ? 0 : padding,
|
||||
exportingFrame: null,
|
||||
renderEmbeddables: true,
|
||||
});
|
||||
@@ -327,14 +358,17 @@ export async function getPNG (
|
||||
return await exportToBlob({
|
||||
elements: scene.elements.filter((el:ExcalidrawElement)=>el.isDeleted !== true),
|
||||
appState: {
|
||||
...scene.appState,
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
? scene.appState?.theme !== "light"
|
||||
: false,
|
||||
...scene.appState,
|
||||
...exportSettings.frameRendering
|
||||
? {frameRendering: exportSettings.frameRendering}
|
||||
: {},
|
||||
},
|
||||
files: filterFiles(scene.files),
|
||||
exportPadding: padding,
|
||||
exportPadding: exportSettings.frameRendering ? 0 : padding,
|
||||
mimeType: "image/png",
|
||||
getDimensions: (width: number, height: number) => ({
|
||||
width: width * scale,
|
||||
@@ -391,10 +425,10 @@ export function embedFontsInSVG(
|
||||
style = document.createElement("style");
|
||||
defs.appendChild(style);
|
||||
}
|
||||
style.innerHTML = `${includesVirgil ? VIRGIL_FONT : ""}${
|
||||
includesCascadia ? CASCADIA_FONT : ""}${
|
||||
includesAssistant ? ASSISTANT_FONT : ""
|
||||
}${includesLocalFont ? plugin.fourthFontDef : ""}`;
|
||||
style.innerHTML = `${includesVirgil ? (VIRGIL_FONT + "\n") : ""}${
|
||||
includesCascadia ? (CASCADIA_FONT + "\n") : ""}${
|
||||
includesAssistant ? (ASSISTANT_FONT + "\n") : ""
|
||||
}${includesLocalFont ? (plugin.fourthFontDef + "\n") : ""}`;
|
||||
}
|
||||
return svg;
|
||||
};
|
||||
@@ -713,7 +747,7 @@ export function isVersionNewerThanOther (version: string, otherVersion: string):
|
||||
|
||||
export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|clippedframe=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|clippedframe=|taskbone)?([^\^\|]*))(.*)/);
|
||||
if(!parts) {
|
||||
return {
|
||||
filepath: fname,
|
||||
@@ -722,6 +756,7 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
hasClippedFrameref: false,
|
||||
blockref: "",
|
||||
hasSectionref: false,
|
||||
sectionref: "",
|
||||
@@ -736,6 +771,7 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS {
|
||||
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
|
||||
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
|
||||
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
|
||||
hasClippedFrameref: (parts[4]==="clippedframe=") || (parts[7]==="clippedframe="),
|
||||
blockref: parts[5],
|
||||
hasSectionref: Boolean(parts[6]),
|
||||
sectionref: parts[8],
|
||||
|
||||
Reference in New Issue
Block a user