mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
8 Commits
1.7.26
...
1.7.28-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0111e264c | ||
|
|
c6196a86a9 | ||
|
|
3926e5c30b | ||
|
|
a1256422fa | ||
|
|
eeb47d4912 | ||
|
|
8ceac4ab31 | ||
|
|
225c6305d1 | ||
|
|
ba9ab61cc9 |
62
.github/ISSUE_TEMPLATE/bug_report.md
vendored
62
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,32 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS including version: [e.g. iOS 15.1, Android 9, Windows 11, etc]
|
||||
- Plugin version:
|
||||
- Obsidian version:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help me improve Excalidraw
|
||||
title: 'BUG: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Your environment**
|
||||
Please run `Command Palette/Show Debug info` in Obsidian and paste the result here.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/feature_request.md
vendored
40
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'FR: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -19,7 +19,7 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
||||
|[](https://youtu.be/Etskjw7a5zo)|[](https://youtu.be/4N6efq1DtH0)|[](https://youtu.be/U2LkBRBk4LY)|
|
||||
| [](https://youtu.be/qiKuqMcNWgU)|[](https://youtu.be/yZQoJg2RCKI)|[](https://youtu.be/6PLGHBH9VZ4) |
|
||||
|[](https://youtu.be/epYNx2FSf2w) | [](https://youtu.be/Amhlv6r9WvM) | [](https://youtu.be/r9oB1SlK1GU) |
|
||||
|[](https://youtu.be/7gJDwNgQ6NU) | | |
|
||||
|[](https://youtu.be/7gJDwNgQ6NU) | [](https://youtu.be/vlC1-iBvIfo) | |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.7.26",
|
||||
"version": "1.7.27",
|
||||
"minAppVersion": "0.15.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@zsviczian/excalidraw": "0.13.0-obsidian",
|
||||
"@zsviczian/excalidraw": "0.13.0-obsidian-1",
|
||||
"clsx": "^1.1.1",
|
||||
"lz-string": "^1.4.4",
|
||||
"monkey-around": "^2.3.0",
|
||||
@@ -26,7 +26,9 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"roughjs": "^4.5.2",
|
||||
"colormaster": "1.2.1"
|
||||
"colormaster": "1.2.1",
|
||||
"chroma-js": "^2.4.2",
|
||||
"gl-matrix": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.12",
|
||||
@@ -41,6 +43,7 @@
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"@rollup/plugin-typescript": "^8.3.0",
|
||||
"@types/js-beautify": "^1.13.3",
|
||||
"@types/chroma-js": "^2.1.4",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import { normalizePath, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { normalizePath, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
|
||||
import { ExcalidrawData } from "./ExcalidrawData";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection } from "./ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
isVersionNewerThanOther,
|
||||
log,
|
||||
scaleLoadedImage,
|
||||
wrapText,
|
||||
wrapTextAtCharLength,
|
||||
} from "./utils/Utils";
|
||||
import { getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { AppState, Point } from "@zsviczian/excalidraw/types/types";
|
||||
@@ -58,6 +59,7 @@ import HSVPlugin from "colormaster/plugins/hsv";
|
||||
import RYBPlugin from "colormaster/plugins/ryb";
|
||||
import CMYKPlugin from "colormaster/plugins/cmyk";
|
||||
import { TInput } from "colormaster/types";
|
||||
import {ConversionResult, svgToExcalidraw} from "./svgToExcalidraw/parser"
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -122,6 +124,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
colorPalette: {};
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) {
|
||||
this.plugin = plugin;
|
||||
@@ -386,10 +389,38 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
template?.appState?.currentItemLinearStrokeSharpness ??
|
||||
this.style.strokeSharpness,
|
||||
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||
colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
|
||||
},
|
||||
files: template?.files ?? {},
|
||||
};
|
||||
|
||||
const generateMD = ():string => {
|
||||
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
|
||||
let outString = "# Text Elements\n";
|
||||
textElements.forEach(te=> {
|
||||
outString += `${te.originalText ?? te.text} ^${te.id}\n\n`;
|
||||
});
|
||||
|
||||
const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link)
|
||||
elementsWithLinks.forEach(el=>{
|
||||
outString += `${el.link} ^${el.id}\n\n`;
|
||||
})
|
||||
|
||||
outString += Object.keys(this.imagesDict).length > 0
|
||||
? "\n# Embedded files\n"
|
||||
: "";
|
||||
|
||||
Object.keys(this.imagesDict).forEach((key: FileId)=> {
|
||||
const item = this.imagesDict[key];
|
||||
if(item.latex) {
|
||||
outString += `${key}: $$${item.latex}$$\n`;
|
||||
} else {
|
||||
outString += `${key}: [[${item.file}]]\n`;
|
||||
}
|
||||
})
|
||||
return outString;
|
||||
}
|
||||
|
||||
return this.plugin.createAndOpenDrawing(
|
||||
params?.filename
|
||||
? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
|
||||
@@ -398,8 +429,8 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
this.plugin.settings.compatibilityMode
|
||||
? JSON.stringify(scene, null, "\t")
|
||||
: frontmatter +
|
||||
(await this.plugin.exportSceneToMD(JSON.stringify(scene, null, "\t"))),
|
||||
: frontmatter + generateMD() +
|
||||
getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -518,7 +549,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @returns
|
||||
*/
|
||||
wrapText(text: string, lineLen: number): string {
|
||||
return wrapText(text, lineLen, this.plugin.settings.forceWrap);
|
||||
return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap);
|
||||
};
|
||||
|
||||
private boxedElement(
|
||||
@@ -1873,6 +1904,16 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
|
||||
return CM(color);
|
||||
}
|
||||
|
||||
importSVG(svgString:string):boolean {
|
||||
const res:ConversionResult = svgToExcalidraw(svgString);
|
||||
if(res.hasErrors) {
|
||||
new Notice (`There were errors while parsing the given SVG:\n${[...res.errors].map((el) => el.innerHTML)}`);
|
||||
return false;
|
||||
}
|
||||
this.copyViewElementsToEAforEditing(res.content);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export async function initExcalidrawAutomate(
|
||||
|
||||
@@ -27,11 +27,12 @@ import {
|
||||
decompress,
|
||||
//getBakPath,
|
||||
getBinaryFileFromDataURL,
|
||||
getContainerElement,
|
||||
getExportTheme,
|
||||
getLinkParts,
|
||||
hasExportTheme,
|
||||
LinkParts,
|
||||
wrapText,
|
||||
wrapTextAtCharLength,
|
||||
} from "./utils/Utils";
|
||||
import { getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import {
|
||||
@@ -52,6 +53,13 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
wrapText,
|
||||
getFontString,
|
||||
getMaxContainerWidth,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
export enum AutoexportPreference {
|
||||
none,
|
||||
both,
|
||||
@@ -210,15 +218,16 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
|
||||
return null;
|
||||
}
|
||||
for (const line of splitText) {
|
||||
if (line.length > maxLineLen) {
|
||||
maxLineLen = line.length;
|
||||
const l = line.trim();
|
||||
if (l.length > maxLineLen) {
|
||||
maxLineLen = l.length;
|
||||
}
|
||||
}
|
||||
return maxLineLen;
|
||||
};
|
||||
|
||||
const wrap = (text: string, lineLen: number) =>
|
||||
lineLen ? wrapText(text, lineLen, false, 0) : text;
|
||||
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
|
||||
|
||||
export class ExcalidrawData {
|
||||
public textElements: Map<
|
||||
@@ -638,12 +647,17 @@ export class ExcalidrawData {
|
||||
//first get scene text elements
|
||||
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
|
||||
for (const te of texts) {
|
||||
const container = getContainerElement(te,this.scene);
|
||||
const originalText =
|
||||
(await this.getText(te.id, false)) ?? te.originalText ?? te.text;
|
||||
(await this.getText(te.id)) ?? te.originalText ?? te.text;
|
||||
const wrapAt = this.textElements.get(te.id)?.wrapAt;
|
||||
this.updateTextElement(
|
||||
te,
|
||||
wrap(originalText, wrapAt),
|
||||
wrapAt ? wrapText(
|
||||
originalText,
|
||||
getFontString(te.fontSize,te.fontFamily),
|
||||
getMaxContainerWidth(container)
|
||||
) : originalText,
|
||||
originalText,
|
||||
forceupdate,
|
||||
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
|
||||
@@ -652,7 +666,6 @@ export class ExcalidrawData {
|
||||
|
||||
private async getText(
|
||||
id: string,
|
||||
wrapResult: boolean = true,
|
||||
): Promise<string> {
|
||||
const text = this.textElements.get(id);
|
||||
if (!text) {
|
||||
@@ -667,7 +680,7 @@ export class ExcalidrawData {
|
||||
});
|
||||
}
|
||||
//console.log("parsed",this.textElements.get(id).parsed);
|
||||
return wrapResult ? wrap(text.parsed, text.wrapAt) : text.parsed;
|
||||
return text.parsed;
|
||||
}
|
||||
//console.log("raw",this.textElements.get(id).raw);
|
||||
return text.raw;
|
||||
@@ -794,7 +807,7 @@ export class ExcalidrawData {
|
||||
if (el.length === 0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
const text = await this.getText(key, false);
|
||||
const text = await this.getText(key);
|
||||
const raw = this.scene.prevTextMode === TextMode.parsed
|
||||
? el[0].rawText
|
||||
: (el[0].originalText ?? el[0].text);
|
||||
@@ -887,7 +900,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
outString +=
|
||||
text.substring(position, parts.value.index) +
|
||||
wrapText(
|
||||
wrapTextAtCharLength(
|
||||
contents,
|
||||
REGEX_LINK.getWrapLength(
|
||||
parts,
|
||||
@@ -1434,7 +1447,7 @@ export class ExcalidrawData {
|
||||
|
||||
const parts = data.linkParts.original.split("#");
|
||||
this.plugin.filesMaster.set(fileId, {
|
||||
path:data.file.path,
|
||||
path:data.file.path + (data.shouldScale()?"":"|100%"),
|
||||
blockrefData: parts.length === 1
|
||||
? null
|
||||
: parts[1],
|
||||
@@ -1479,16 +1492,18 @@ export class ExcalidrawData {
|
||||
}
|
||||
if (this.plugin.filesMaster.has(fileId)) {
|
||||
const masterFile = this.plugin.filesMaster.get(fileId);
|
||||
if (!this.app.vault.getAbstractFileByPath(masterFile.path)) {
|
||||
const path = masterFile.path.split("|")[0].split("#")[0];
|
||||
if (!this.app.vault.getAbstractFileByPath(path)) {
|
||||
this.plugin.filesMaster.delete(fileId);
|
||||
return true;
|
||||
} // the file no longer exists
|
||||
const fixScale = masterFile.path.endsWith("100%");
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
masterFile.blockrefData
|
||||
? masterFile.path + "#" + masterFile.blockrefData
|
||||
: masterFile.path
|
||||
(masterFile.blockrefData
|
||||
? path + "#" + masterFile.blockrefData
|
||||
: path) + (fixScale?"|100%":"")
|
||||
);
|
||||
this.files.set(fileId, embeddedFile);
|
||||
return true;
|
||||
|
||||
@@ -92,6 +92,7 @@ import { ObsidianMenu } from "./menu/ObsidianMenu";
|
||||
import { ToolsPanel } from "./menu/ToolsPanel";
|
||||
import { ScriptEngine } from "./Scripts";
|
||||
import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer";
|
||||
import { MenuLinks } from "./menu/menuLinks";
|
||||
|
||||
export enum TextMode {
|
||||
parsed = "parsed",
|
||||
@@ -255,6 +256,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private linkAction_Element: HTMLElement;
|
||||
public compatibilityMode: boolean = false;
|
||||
private obsidianMenu: ObsidianMenu;
|
||||
private menuLinks: MenuLinks;
|
||||
|
||||
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
|
||||
private isEditingTextResetTimer: NodeJS.Timeout = null;
|
||||
@@ -306,7 +308,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!this.getScene || !this.file) {
|
||||
return;
|
||||
}
|
||||
//@ts-ignore
|
||||
if (app.isMobile) {
|
||||
const prompt = new Prompt(
|
||||
app,
|
||||
@@ -651,7 +652,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||
this.toolsPanelRef.current.setFullscreen(true);
|
||||
}
|
||||
if (app.isMobile) {
|
||||
if (this.plugin.device.isPhone) {
|
||||
if(Platform.isIosApp) {
|
||||
this.restoreMobileLeaves();
|
||||
app.workspace.getLayout().main.children
|
||||
@@ -735,7 +736,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||
this.toolsPanelRef.current.setFullscreen(false);
|
||||
}
|
||||
if (app.isMobile) {
|
||||
if (this.plugin.device.isPhone) {
|
||||
this.restoreMobileLeaves();
|
||||
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
||||
if (oldStylesheet) {
|
||||
@@ -2014,6 +2015,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let currentPosition = { x: 0, y: 0 };
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const toolsPanelRef = React.useRef(null);
|
||||
const menuLinksRef = React.useRef(null);
|
||||
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
@@ -2028,6 +2031,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
this.toolsPanelRef = toolsPanelRef;
|
||||
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef);
|
||||
this.menuLinks = new MenuLinks(this.plugin, menuLinksRef);
|
||||
|
||||
//excalidrawRef readypromise based on
|
||||
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
||||
@@ -2354,7 +2358,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
elements,
|
||||
commitToHistory: true,
|
||||
},
|
||||
false,
|
||||
true, //set to true because svtToExcalidraw generates a legacy Excalidraw object
|
||||
true
|
||||
);
|
||||
|
||||
@@ -2731,7 +2735,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
loadScene: false,
|
||||
saveScene: false,
|
||||
saveAsScene: false,
|
||||
export: { saveFileToDisk: false },
|
||||
export: false,
|
||||
saveAsImage: false,
|
||||
saveToActiveFile: false,
|
||||
},
|
||||
@@ -2782,6 +2786,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
},
|
||||
libraryReturnUrl: "app://obsidian.md",
|
||||
autoFocus: true,
|
||||
hideWelcomeScreen: true,
|
||||
renderMenuLinks: null, //this.menuLinks.render,
|
||||
onChange: (et: ExcalidrawElement[], st: AppState) => {
|
||||
const canvasColorChangeHook = () => {
|
||||
if(this.plugin.ea.onCanvasColorChangeHook) {
|
||||
@@ -2796,7 +2802,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (this.semaphores.justLoaded) {
|
||||
this.semaphores.justLoaded = false;
|
||||
if (!this.semaphores.preventAutozoom) {
|
||||
this.zoomToFit(false);
|
||||
this.zoomToFit(false,true);
|
||||
}
|
||||
this.previousSceneVersion = this.getSceneVersion(et);
|
||||
this.previousBackgroundColor = st.viewBackgroundColor;
|
||||
@@ -3396,13 +3402,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
public zoomToFit(delay: boolean = true) {
|
||||
public zoomToFit(delay: boolean = true, justLoaded: boolean = false) {
|
||||
const api = this.excalidrawAPI;
|
||||
if (!api || !this.excalidrawRef || this.semaphores.isEditingText) {
|
||||
return;
|
||||
}
|
||||
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
||||
const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000);
|
||||
if((app.isMobile && elements.length>1000) || elements.length>2500) {
|
||||
if(justLoaded) api.scrollToContent();
|
||||
return;
|
||||
}
|
||||
if (delay) {
|
||||
//time for the DOM to render, I am sure there is a more elegant solution
|
||||
setTimeout(
|
||||
|
||||
@@ -210,7 +210,7 @@ COLOR_NAMES.set("white", "#ffffff");
|
||||
COLOR_NAMES.set("whitesmoke", "#f5f5f5");
|
||||
COLOR_NAMES.set("yellow", "#ffff00");
|
||||
COLOR_NAMES.set("yellowgreen", "#9acd32");
|
||||
export const DEFAULT_MD_EMBED_CSS = `.excalidraw-md-host{padding:0px 10px}.excalidraw-md-footer{height:5px}foreignObject{background-color:transparent}p{display:block;margin-block-start:1em;margin-block-end:1em;margin-inline-start:0px;margin-inline-end:0px;color:inherit}table,tr,th,td{color:inherit;border:1px solid;border-collapse:collapse;padding:3px}th{font-weight:bold;border-bottom:double;background-color:silver}.copy-code-button{display:none}code[class*=language-],pre[class*=language-]{color:#393a34;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;font-size:.9em;line-height:1.2em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre>code[class*=language-]{font-size:1em}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{background:#C1DEF1}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{background:#C1DEF1}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;background-color:#0000001a}:not(pre)>code[class*=language-]{padding:.2em;padding-top:1px;padding-bottom:1px;background:#f8f8f8;border:1px solid #dddddd}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:green;font-style:italic}.token.namespace{opacity:.7}.token.string{color:#a31515}.token.punctuation,.token.operator{color:#393a34}.token.url,.token.symbol,.token.number,.token.boolean,.token.variable,.token.constant,.token.inserted{color:#36acaa}.token.atrule,.token.keyword,.token.attr-value,.language-autohotkey .token.selector,.language-json .token.boolean,.language-json .token.number,code[class*=language-css]{color:#00f}.token.function{color:#393a34}.token.deleted,.language-autohotkey .token.tag{color:#9a050f}.token.selector,.language-autohotkey .token.keyword{color:#00009f}.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.class-name,.language-json .token.property{color:#2b91af}.token.tag,.token.selector{color:maroon}.token.attr-name,.token.property,.token.regex,.token.entity{color:red}.token.directive.tag .tag{background:#ffff00;color:#393a34}.line-numbers.line-numbers .line-numbers-rows{border-right-color:#a5a5a5}.line-numbers .line-numbers-rows>span:before{color:#2b91af}.line-highlight.line-highlight{background:rgba(193,222,241,.2);background:-webkit-linear-gradient(left,rgba(193,222,241,.2) 70%,rgba(221,222,241,0));background:linear-gradient(to right,rgba(193,222,241,.2) 70%,rgba(221,222,241,0))}blockquote{ font-style:italic;background-color:rgb(46,43,42,0.1);margin:0;margin-left:1em;border-radius:0 4px 4px 0;border:1px solid hsl(0,80%,32%);border-left-width:8px;border-top-width:0px;border-right-width:0px;border-bottom-width:0px;padding:10px 20px;margin-inline-start:30px;margin-inline-end:30px;}`;
|
||||
export const DEFAULT_MD_EMBED_CSS = `.snw-reference{display: none;}.excalidraw-md-host{padding:0px 10px}.excalidraw-md-footer{height:5px}foreignObject{background-color:transparent}p{display:block;margin-block-start:1em;margin-block-end:1em;margin-inline-start:0px;margin-inline-end:0px;color:inherit}table,tr,th,td{color:inherit;border:1px solid;border-collapse:collapse;padding:3px}th{font-weight:bold;border-bottom:double;background-color:silver}.copy-code-button{display:none}code[class*=language-],pre[class*=language-]{color:#393a34;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;font-size:.9em;line-height:1.2em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre>code[class*=language-]{font-size:1em}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{background:#C1DEF1}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{background:#C1DEF1}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;background-color:#0000001a}:not(pre)>code[class*=language-]{padding:.2em;padding-top:1px;padding-bottom:1px;background:#f8f8f8;border:1px solid #dddddd}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:green;font-style:italic}.token.namespace{opacity:.7}.token.string{color:#a31515}.token.punctuation,.token.operator{color:#393a34}.token.url,.token.symbol,.token.number,.token.boolean,.token.variable,.token.constant,.token.inserted{color:#36acaa}.token.atrule,.token.keyword,.token.attr-value,.language-autohotkey .token.selector,.language-json .token.boolean,.language-json .token.number,code[class*=language-css]{color:#00f}.token.function{color:#393a34}.token.deleted,.language-autohotkey .token.tag{color:#9a050f}.token.selector,.language-autohotkey .token.keyword{color:#00009f}.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.class-name,.language-json .token.property{color:#2b91af}.token.tag,.token.selector{color:maroon}.token.attr-name,.token.property,.token.regex,.token.entity{color:red}.token.directive.tag .tag{background:#ffff00;color:#393a34}.line-numbers.line-numbers .line-numbers-rows{border-right-color:#a5a5a5}.line-numbers .line-numbers-rows>span:before{color:#2b91af}.line-highlight.line-highlight{background:rgba(193,222,241,.2);background:-webkit-linear-gradient(left,rgba(193,222,241,.2) 70%,rgba(221,222,241,0));background:linear-gradient(to right,rgba(193,222,241,.2) 70%,rgba(221,222,241,0))}blockquote{ font-style:italic;background-color:rgb(46,43,42,0.1);margin:0;margin-left:1em;border-radius:0 4px 4px 0;border:1px solid hsl(0,80%,32%);border-left-width:8px;border-top-width:0px;border-right-width:0px;border-bottom-width:0px;padding:10px 20px;margin-inline-start:30px;margin-inline-end:30px;}`;
|
||||
export const SCRIPTENGINE_ICON = `<g transform="translate(-8,-8)"><path d="M24.318 37.983c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749m.126-.104c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749" fill="none" stroke-width="2" stroke-linecap="round" stroke="currentColor"/><path d="M81.235 56.502a23.3 23.3 0 0 1-1.46 8.068 20.785 20.785 0 0 1-1.762 3.72 24.068 24.068 0 0 1-5.337 6.26 22.575 22.575 0 0 1-3.449 2.358 23.726 23.726 0 0 1-7.803 2.803 24.719 24.719 0 0 1-8.333 0 24.102 24.102 0 0 1-4.028-1.074 23.71 23.71 0 0 1-3.776-1.729 23.259 23.259 0 0 1-6.369-5.265 23.775 23.775 0 0 1-2.416-3.353 24.935 24.935 0 0 1-1.762-3.72 23.765 23.765 0 0 1-1.083-3.981 23.454 23.454 0 0 1 0-8.173c.252-1.336.604-2.698 1.083-3.956a24.935 24.935 0 0 1 1.762-3.72 22.587 22.587 0 0 1 2.416-3.378c.881-1.048 1.888-2.017 2.946-2.908a24.38 24.38 0 0 1 3.423-2.357 23.71 23.71 0 0 1 3.776-1.73 21.74 21.74 0 0 1 4.028-1.047 23.437 23.437 0 0 1 8.333 0 24.282 24.282 0 0 1 7.803 2.777 26.198 26.198 0 0 1 3.45 2.357 24.62 24.62 0 0 1 5.336 6.287 20.785 20.785 0 0 1 1.762 3.72 21.32 21.32 0 0 1 1.083 3.955c.251 1.336.302 3.405.377 4.086.05.681.05-.68 0 0" fill="none" stroke-width="4" stroke-linecap="round" stroke="currentColor"/><path d="M69.404 56.633c-6.596-3.3-13.216-6.6-19.51-9.744m19.51 9.744c-6.747-3.379-13.493-6.758-19.51-9.744m0 0v19.489m0-19.49v19.49m0 0c4.355-2.148 8.71-4.322 19.51-9.745m-19.51 9.745c3.978-1.965 7.93-3.956 19.51-9.745m0 0h0m0 0h0" fill="currentColor" stroke-linecap="round" stroke="currentColor" stroke-width="4"/></g>`;
|
||||
export const DISK_ICON_NAME = "disk";
|
||||
export const DISK_ICON = `<path fill="none" stroke="currentColor" fill="#fff" d="M0 0h100v100H0z"/><path fill="none" stroke="currentColor" d="M20.832 4.168c21.824.145 43.645.289 74.68.5m-74.68-.5c17.09.113 34.176.227 74.68.5m0 0c.094 27.3.191 54.602.32 91.164m-.32-91.164c.113 32.633.23 65.27.32 91.164m0 0H4.168m91.664 0H4.168m0 0v-75m0 75v-75m0 0L20.832 4.168M4.168 20.832L20.832 4.168M20.832 4.168h58.336m-58.336 0h58.336m0 0v25m0-25v25m0 0H20.832m58.336 0H20.832m0 0v-25m0 25v-25" stroke-width="1.66668" /><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664v16.664H29.168"/><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664m-16.664 0h16.664m0 0v16.664m0-16.664v16.664m0 0H29.168m16.664 0H29.168m0 0V4.168m0 16.664V4.168M12.5 54.168h75m-75 0h75m0 0v41.664m0-41.664v41.664m0 0h-75m75 0h-75m0 0V54.168m0 41.664V54.168M20.832 62.5c20.11-.18 40.219-.36 55.68-.5m-55.68.5c14.656-.133 29.313-.262 55.68-.5M20.832 71.332c13.098-.117 26.2-.234 55.68-.5m-55.68.5l55.68-.5M21.117 79.582c20.645-.184 41.285-.371 55.68-.5m-55.68.5c18.153-.16 36.301-.324 55.68-.5" stroke-width="1.66668"/>`;
|
||||
|
||||
54
src/dialogs/ImportSVGDialog.ts
Normal file
54
src/dialogs/ImportSVGDialog.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
|
||||
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
},
|
||||
]);
|
||||
this.setPlaceholder(t("SELECT_DRAWING"));
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
return (this.app.vault.getFiles() || []).filter(
|
||||
(f: TFile) => f.extension === "svg" &&
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
|
||||
!f.path.match(REG_LINKINDEX_INVALIDCHARS),
|
||||
);
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
async onChooseItem(item: TFile, event: KeyboardEvent): Promise<void> {
|
||||
if(!item) return;
|
||||
const ea = this.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(this.view);
|
||||
const svg = await app.vault.read(item);
|
||||
if(!svg || svg === "") return;
|
||||
ea.importSVG(svg);
|
||||
ea.addElementsToView(true, true, true);
|
||||
}
|
||||
|
||||
public start(view: ExcalidrawView) {
|
||||
this.view = view;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,23 @@ Thank you & Enjoy!
|
||||
`;
|
||||
|
||||
export const RELEASE_NOTES: { [k: string]: string } = {
|
||||
Intro: `I want to help you keep up with all the updates. After installing each release, you'll be prompted with a summary of new features and fixes. You can disable these popup messages in plugin settings.
|
||||
Intro: `After each update you'll be prompted with the release notes. You can disable this in plugin settings.
|
||||
|
||||
I develop this plugin as a hobby, spending most of my free time doing this. If you'd like to contribute to the on-going work, I have a simple membership scheme with Bronze, Silver and Gold tiers. Many of you have already bought me a coffee. THANK YOU! It really means a lot to me! If you find this plugin valuable, please consider supporting me.
|
||||
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
|
||||
|
||||
<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>
|
||||
`,
|
||||
"1.7.27":`## New
|
||||
- Import SVG drawing as an Excalidraw object. [#679](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/679)
|
||||
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/vlC1-iBvIfo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Large drawings freeze on the iPad when opening the file. I implemented a workaround whereby Excalidraw will avoid zoom-to-fit drawings with over 1000 elements. [#863](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/863)
|
||||
- Reintroduced copy/paste to the context menu
|
||||
`,
|
||||
"1.7.26":`## Fixed
|
||||
- Transcluded block with a parent bullet does not embed sub-bullet [#853](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/853)
|
||||
- Transcluded text will now exclude ^block-references at end of lines
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ReleaseNotes extends Modal {
|
||||
const message = this.version
|
||||
? Object.keys(RELEASE_NOTES)
|
||||
.filter((key) => key === "Intro" || isVersionNewerThanOther(key,prevRelease))
|
||||
.map((key: string) => `# ${key}\n${RELEASE_NOTES[key]}`)
|
||||
.map((key: string) => `${key==="Intro" ? "" : `# ${key}\n`}${RELEASE_NOTES[key]}`)
|
||||
.slice(0, 10)
|
||||
.join("\n\n---\n")
|
||||
: FIRST_RUN;
|
||||
|
||||
@@ -224,8 +224,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "addImage",
|
||||
code: "addImage(topX: number, topY: number, imageFile: TFile): Promise<string>;",
|
||||
desc: null,
|
||||
code: "addImage(topX: number, topY: number, imageFile: TFile, scale: boolean): Promise<string>;",
|
||||
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -53,6 +53,7 @@ export default {
|
||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
||||
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_LATEX:
|
||||
"Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
|
||||
|
||||
44
src/main.ts
44
src/main.ts
@@ -58,6 +58,7 @@ import {
|
||||
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
||||
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
||||
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
||||
import { ImportSVGDialog } from "./dialogs/ImportSVGDialog";
|
||||
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
||||
import {
|
||||
initExcalidrawAutomate,
|
||||
@@ -139,6 +140,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
private openDialog: OpenFileDialog;
|
||||
public insertLinkDialog: InsertLinkDialog;
|
||||
public insertImageDialog: InsertImageDialog;
|
||||
public importSVGDialog: ImportSVGDialog;
|
||||
public insertMDDialog: InsertMDDialog;
|
||||
public activeExcalidrawView: ExcalidrawView = null;
|
||||
public lastActiveExcalidrawFilePath: string = null;
|
||||
@@ -166,6 +168,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
||||
public leafChangeTimeout: NodeJS.Timeout = null;
|
||||
private forceSaveCommand:Command;
|
||||
public device: {
|
||||
isDesktop: boolean,
|
||||
isPhone: boolean,
|
||||
isTablet: boolean,
|
||||
isMobile: boolean,
|
||||
isLinux: boolean,
|
||||
isMacOS: boolean,
|
||||
isWindows: boolean,
|
||||
isIOS: boolean,
|
||||
isAndroid: boolean
|
||||
};
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
@@ -176,8 +189,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.equationsMaster = new Map<FileId, string>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public getPackage(win:Window):Packages {
|
||||
if(win===window) {
|
||||
return {react, reactDOM, excalidrawLib};
|
||||
@@ -197,6 +208,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async onload() {
|
||||
this.device = {
|
||||
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
|
||||
isPhone: document.body.hasClass("is-phone"),
|
||||
isTablet: document.body.hasClass("is-tablet"),
|
||||
isMobile: document.body.hasClass("is-mobile"), //running Obsidian Mobile, need to also check isTablet
|
||||
isLinux: document.body.hasClass("mod-linux") && ! document.body.hasClass("is-android"),
|
||||
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
|
||||
isWindows: document.body.hasClass("mod-windows"),
|
||||
isIOS: document.body.hasClass("is-ios"),
|
||||
isAndroid: document.body.hasClass("is-android")
|
||||
}
|
||||
|
||||
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
||||
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
||||
addIcon(DISK_ICON_NAME, DISK_ICON);
|
||||
@@ -691,6 +714,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.openDialog = new OpenFileDialog(this.app, this);
|
||||
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
||||
this.insertImageDialog = new InsertImageDialog(this);
|
||||
this.importSVGDialog = new ImportSVGDialog(this);
|
||||
this.insertMDDialog = new InsertMDDialog(this);
|
||||
|
||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
||||
@@ -1229,6 +1253,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "import-svg",
|
||||
name: t("IMPORT_SVG"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
this.importSVGDialog.start(view);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "release-notes",
|
||||
name: t("READ_RELEASE_NOTES"),
|
||||
|
||||
@@ -26,9 +26,9 @@ export class ActionButton extends React.Component<ButtonProps, ButtonState> {
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
width: "fit-content",
|
||||
padding: "2px",
|
||||
margin: "4px",
|
||||
//width: "fit-content",
|
||||
//padding: "2px",
|
||||
//margin: "4px",
|
||||
}}
|
||||
className="ToolIcon_type_button ToolIcon_size_small ToolIcon_type_button--show ToolIcon"
|
||||
title={this.props.title}
|
||||
|
||||
File diff suppressed because one or more lines are too long
21
src/menu/MenuLinks.tsx
Normal file
21
src/menu/MenuLinks.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
|
||||
|
||||
export class MenuLinks {
|
||||
plugin: ExcalidrawPlugin;
|
||||
ref: React.MutableRefObject<any>;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, ref: React.MutableRefObject<any>) {
|
||||
this.plugin = plugin;
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
render = (isMobile: boolean, appState: AppState) => {
|
||||
return (
|
||||
<div>Hello</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
className="Island App-menu__left scrollbar"
|
||||
style={{
|
||||
maxHeight: "350px",
|
||||
backgroundColor: "transparent",
|
||||
width: "initial",
|
||||
//@ts-ignore
|
||||
"--padding": 2,
|
||||
display: this.state.minimized ? "none" : "block",
|
||||
@@ -473,6 +473,15 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
icon={ICONS.copyElementLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"import-svg"}
|
||||
title={t("IMPORT_SVG")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.props.view.plugin.importSVGDialog.start(this.props.view);
|
||||
}}
|
||||
icon={ICONS.importSVG}
|
||||
view={this.props.view}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
{this.renderScriptButtons(false)}
|
||||
|
||||
133
src/svgToExcalidraw/attributes.ts
Normal file
133
src/svgToExcalidraw/attributes.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import chroma from "chroma-js";
|
||||
import { ExcalidrawElementBase } from "./elements/ExcalidrawElement";
|
||||
|
||||
export function hexWithAlpha(color: string, alpha: number): string {
|
||||
return chroma(color).alpha(alpha).css();
|
||||
}
|
||||
|
||||
export function has(el: Element, attr: string): boolean {
|
||||
return el.hasAttribute(attr);
|
||||
}
|
||||
|
||||
export function get(el: Element, attr: string, backup?: string): string {
|
||||
return el.getAttribute(attr) || backup || "";
|
||||
}
|
||||
|
||||
export function getNum(el: Element, attr: string, backup?: number): number {
|
||||
const numVal = Number(get(el, attr));
|
||||
return numVal === NaN ? backup || 0 : numVal;
|
||||
}
|
||||
|
||||
const presAttrs = {
|
||||
stroke: "stroke",
|
||||
"stroke-opacity": "stroke-opacity",
|
||||
"stroke-width": "stroke-width",
|
||||
fill: "fill",
|
||||
"fill-opacity": "fill-opacity",
|
||||
opacity: "opacity",
|
||||
} as const;
|
||||
|
||||
type ExPartialElement = Partial<ExcalidrawElementBase>;
|
||||
|
||||
type AttrHandlerArgs = {
|
||||
el: Element;
|
||||
exVals: ExPartialElement;
|
||||
};
|
||||
|
||||
type PresAttrHandlers = {
|
||||
[key in keyof typeof presAttrs]: (args: AttrHandlerArgs) => void;
|
||||
};
|
||||
|
||||
const attrHandlers: PresAttrHandlers = {
|
||||
stroke: ({ el, exVals }) => {
|
||||
const strokeColor = get(el, "stroke");
|
||||
|
||||
exVals.strokeColor = has(el, "stroke-opacity")
|
||||
? hexWithAlpha(strokeColor, getNum(el, "stroke-opacity"))
|
||||
: strokeColor;
|
||||
},
|
||||
|
||||
"stroke-opacity": ({ el, exVals }) => {
|
||||
exVals.strokeColor = hexWithAlpha(
|
||||
get(el, "stroke", "#000000"),
|
||||
getNum(el, "stroke-opacity"),
|
||||
);
|
||||
},
|
||||
|
||||
"stroke-width": ({ el, exVals }) => {
|
||||
exVals.strokeWidth = getNum(el, "stroke-width");
|
||||
},
|
||||
|
||||
fill: ({ el, exVals }) => {
|
||||
const fill = get(el, `fill`);
|
||||
|
||||
exVals.backgroundColor = fill === "none" ? "#00000000" : fill;
|
||||
},
|
||||
|
||||
"fill-opacity": ({ el, exVals }) => {
|
||||
exVals.backgroundColor = hexWithAlpha(
|
||||
get(el, "fill", "#000000"),
|
||||
getNum(el, "fill-opacity"),
|
||||
);
|
||||
},
|
||||
|
||||
opacity: ({ el, exVals }) => {
|
||||
exVals.opacity = getNum(el, "opacity", 100);
|
||||
},
|
||||
};
|
||||
|
||||
// Presentation Attributes for SVG Elements:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation
|
||||
export function presAttrsToElementValues(
|
||||
el: Element,
|
||||
): Partial<ExcalidrawElementBase> {
|
||||
const exVals = [...el.attributes].reduce((exVals, attr) => {
|
||||
const name = attr.name;
|
||||
|
||||
if (Object.keys(attrHandlers).includes(name)) {
|
||||
attrHandlers[name as keyof PresAttrHandlers]({ el, exVals });
|
||||
}
|
||||
|
||||
return exVals;
|
||||
}, {} as ExPartialElement);
|
||||
|
||||
return exVals;
|
||||
}
|
||||
|
||||
type FilterAttrs = Partial<
|
||||
Pick<ExcalidrawElementBase, "x" | "y" | "width" | "height">
|
||||
>;
|
||||
|
||||
export function filterAttrsToElementValues(el: Element): FilterAttrs {
|
||||
const filterVals: FilterAttrs = {};
|
||||
|
||||
if (has(el, "x")) {
|
||||
filterVals.x = getNum(el, "x");
|
||||
}
|
||||
|
||||
if (has(el, "y")) {
|
||||
filterVals.y = getNum(el, "y");
|
||||
}
|
||||
|
||||
if (has(el, "width")) {
|
||||
filterVals.width = getNum(el, "width");
|
||||
}
|
||||
|
||||
if (has(el, "height")) {
|
||||
filterVals.height = getNum(el, "height");
|
||||
}
|
||||
|
||||
return filterVals;
|
||||
}
|
||||
|
||||
export function pointsAttrToPoints(el: Element): number[][] {
|
||||
let points: number[][] = [];
|
||||
|
||||
if (has(el, "points")) {
|
||||
points = get(el, "points")
|
||||
.split(" ")
|
||||
.map((p) => p.split(",").map(parseFloat));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
117
src/svgToExcalidraw/elements/ExcalidrawElement.ts
Normal file
117
src/svgToExcalidraw/elements/ExcalidrawElement.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { randomId, randomInteger } from "../utils";
|
||||
|
||||
import { ExcalidrawLinearElement, FillStyle, GroupId, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
export type ExcalidrawElementBase = {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
width: number;
|
||||
height: number;
|
||||
angle: number;
|
||||
/** Random integer used to seed shape generation so that the roughjs shape
|
||||
doesn't differ across renders. */
|
||||
seed: number;
|
||||
/** Integer that is sequentially incremented on each change. Used to reconcile
|
||||
elements during collaboration or when saving to server. */
|
||||
version: number;
|
||||
/** Random integer that is regenerated on each change.
|
||||
Used for deterministic reconciliation of updates during collaboration,
|
||||
in case the versions (see above) are identical. */
|
||||
versionNonce: number;
|
||||
isDeleted: boolean;
|
||||
/** List of groups the element belongs to.
|
||||
Ordered from deepest to shallowest. */
|
||||
groupIds: GroupId[];
|
||||
/** Ids of (linear) elements that are bound to this element. */
|
||||
boundElementIds: ExcalidrawLinearElement["id"][] | null;
|
||||
};
|
||||
|
||||
export type ExcalidrawRectangle = ExcalidrawElementBase & {
|
||||
type: "rectangle";
|
||||
};
|
||||
|
||||
export type ExcalidrawLine = ExcalidrawElementBase & {
|
||||
type: "line";
|
||||
points: readonly Point[];
|
||||
};
|
||||
|
||||
export type ExcalidrawEllipse = ExcalidrawElementBase & {
|
||||
type: "ellipse";
|
||||
};
|
||||
|
||||
export type ExcalidrawGenericElement =
|
||||
| ExcalidrawRectangle
|
||||
| ExcalidrawEllipse
|
||||
| ExcalidrawLine
|
||||
| ExcalidrawDraw;
|
||||
|
||||
export type ExcalidrawDraw = ExcalidrawElementBase & {
|
||||
type: "line";
|
||||
points: readonly Point[];
|
||||
};
|
||||
|
||||
export function createExElement(): ExcalidrawElementBase {
|
||||
return {
|
||||
id: randomId(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#000000",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
strokeSharpness: "sharp",
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
width: 0,
|
||||
height: 0,
|
||||
angle: 0,
|
||||
seed: randomInteger(),
|
||||
version: 0,
|
||||
versionNonce: 0,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
boundElementIds: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createExRect(): ExcalidrawRectangle {
|
||||
return {
|
||||
...createExElement(),
|
||||
type: "rectangle",
|
||||
};
|
||||
}
|
||||
|
||||
export function createExLine(): ExcalidrawLine {
|
||||
return {
|
||||
...createExElement(),
|
||||
type: "line",
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function createExEllipse(): ExcalidrawEllipse {
|
||||
return {
|
||||
...createExElement(),
|
||||
type: "ellipse",
|
||||
};
|
||||
}
|
||||
|
||||
export function createExDraw(): ExcalidrawDraw {
|
||||
return {
|
||||
...createExElement(),
|
||||
type: "line",
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
21
src/svgToExcalidraw/elements/ExcalidrawScene.ts
Normal file
21
src/svgToExcalidraw/elements/ExcalidrawScene.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
|
||||
|
||||
class ExcalidrawScene {
|
||||
type = "excalidraw";
|
||||
version = 2;
|
||||
source = "https://excalidraw.com";
|
||||
elements: ExcalidrawGenericElement[] = [];
|
||||
|
||||
constructor(elements:any = []) {
|
||||
this.elements = elements;
|
||||
}
|
||||
|
||||
toExJSON(): any {
|
||||
return {
|
||||
...this,
|
||||
elements: this.elements.map((el) => ({ ...el })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ExcalidrawScene;
|
||||
23
src/svgToExcalidraw/elements/Group.ts
Normal file
23
src/svgToExcalidraw/elements/Group.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { randomId } from "../utils";
|
||||
import { presAttrsToElementValues } from "../attributes";
|
||||
import { ExcalidrawElementBase } from "../elements/ExcalidrawElement";
|
||||
|
||||
export function getGroupAttrs(groups: Group[]): any {
|
||||
return groups.reduce((acc, { element }) => {
|
||||
const elVals = presAttrsToElementValues(element);
|
||||
|
||||
return { ...acc, ...elVals };
|
||||
}, {} as Partial<ExcalidrawElementBase>);
|
||||
}
|
||||
|
||||
class Group {
|
||||
id = randomId();
|
||||
|
||||
element: Element;
|
||||
|
||||
constructor(element: Element) {
|
||||
this.element = element;
|
||||
}
|
||||
}
|
||||
|
||||
export default Group;
|
||||
5
src/svgToExcalidraw/elements/index.ts
Normal file
5
src/svgToExcalidraw/elements/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as path from "./path";
|
||||
|
||||
export default {
|
||||
path,
|
||||
};
|
||||
35
src/svgToExcalidraw/elements/path/index.ts
Normal file
35
src/svgToExcalidraw/elements/path/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { RawElement } from "../../types";
|
||||
import { getElementBoundaries } from "../utils";
|
||||
import pathToPoints from "./utils/path-to-points";
|
||||
|
||||
const parse = (node: Element) => {
|
||||
const data = node.getAttribute("d");
|
||||
const backgroundColor = node.getAttribute("fill");
|
||||
const strokeColor = node.getAttribute("stroke");
|
||||
|
||||
return {
|
||||
data: data || "",
|
||||
backgroundColor:
|
||||
(backgroundColor !== "currentColor" && backgroundColor) || "transparent",
|
||||
strokeColor: (strokeColor !== "currentColor" && strokeColor) || "#000000",
|
||||
};
|
||||
};
|
||||
|
||||
export const convert = (node: Element): RawElement[] => {
|
||||
const { data, backgroundColor, strokeColor } = parse(node);
|
||||
const elementsPoints = pathToPoints(data);
|
||||
|
||||
return elementsPoints.map((points) => {
|
||||
const boundaries = getElementBoundaries(points);
|
||||
|
||||
return {
|
||||
type: "line",
|
||||
roughness: 0,
|
||||
strokeSharpness: "sharp",
|
||||
points,
|
||||
backgroundColor,
|
||||
strokeColor,
|
||||
...boundaries,
|
||||
};
|
||||
});
|
||||
};
|
||||
66
src/svgToExcalidraw/elements/path/utils/bezier.ts
Normal file
66
src/svgToExcalidraw/elements/path/utils/bezier.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { safeNumber } from "../../../utils";
|
||||
|
||||
/**
|
||||
* Get a point at a given section of a cubic bezier curve.
|
||||
* This function only supports two dimensions curves
|
||||
* @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
|
||||
*/
|
||||
const getPointOfCubicCurve = (
|
||||
controlPoints: number[][],
|
||||
section: number,
|
||||
): number[] =>
|
||||
Array.from({ length: 2 }).map((v, i) => {
|
||||
const point =
|
||||
controlPoints[0][i] * (1 - section) ** 3 +
|
||||
3 * controlPoints[1][i] * section * (1 - section) ** 2 +
|
||||
3 * controlPoints[2][i] * section ** 2 * (1 - section) +
|
||||
controlPoints[3][i] * section ** 3;
|
||||
|
||||
return safeNumber(point);
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a point at a given section of a quadratic bezier curve.
|
||||
* This function only supports two dimensions curves
|
||||
* @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
|
||||
*/
|
||||
const getPointOfQuadraticCurve = (
|
||||
controlPoints: number[][],
|
||||
section: number,
|
||||
): number[] =>
|
||||
Array.from({ length: 2 }).map((v, i) => {
|
||||
const point =
|
||||
controlPoints[0][i] * (1 - section) ** 2 +
|
||||
2 * controlPoints[1][i] * section * (1 - section) +
|
||||
controlPoints[2][i] * section ** 2;
|
||||
|
||||
return safeNumber(point);
|
||||
});
|
||||
|
||||
/**
|
||||
* Get list of points for a cubic bézier curve.
|
||||
* Starting point is not returned
|
||||
*/
|
||||
export const curveToPoints = (
|
||||
type: "cubic" | "quadratic",
|
||||
controlPoints: number[][],
|
||||
nbPoints = 10,
|
||||
): number[][] => {
|
||||
if (nbPoints <= 0) {
|
||||
throw new Error("Requested amount of points must be positive");
|
||||
} else if (nbPoints > 100) {
|
||||
nbPoints = 100;
|
||||
}
|
||||
|
||||
return Array.from({ length: nbPoints }, (value, index) => {
|
||||
const section = safeNumber(((100 / nbPoints) * (index + 1)) / 100);
|
||||
|
||||
if (type === "cubic") {
|
||||
return getPointOfCubicCurve(controlPoints, section);
|
||||
} else if (type === "quadratic") {
|
||||
return getPointOfQuadraticCurve(controlPoints, section);
|
||||
}
|
||||
|
||||
throw new Error("Invalid bézier curve type requested");
|
||||
});
|
||||
};
|
||||
133
src/svgToExcalidraw/elements/path/utils/ellipse.ts
Normal file
133
src/svgToExcalidraw/elements/path/utils/ellipse.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
const degreeToRadian = (degree: number): number => (degree * Math.PI) / 180;
|
||||
|
||||
/**
|
||||
* Get each possible ellipses center points given two points and ellipse radius
|
||||
* @see https://math.stackexchange.com/questions/2240031/solving-an-equation-for-an-ellipse
|
||||
*/
|
||||
export const getEllipsesCenter = (
|
||||
curX: number,
|
||||
curY: number,
|
||||
destX: number,
|
||||
destY: number,
|
||||
radiusX: number,
|
||||
radiusY: number,
|
||||
): number[][] => [
|
||||
[
|
||||
(curX + destX) / 2 +
|
||||
((radiusX * (curY - destY)) / (2 * radiusY)) *
|
||||
Math.sqrt(
|
||||
4 /
|
||||
((curX - destX) ** 2 / radiusX ** 2 +
|
||||
(curY - destY) ** 2 / radiusY ** 2) -
|
||||
1,
|
||||
),
|
||||
(curY + destY) / 2 -
|
||||
((radiusY * (curX - destX)) / (2 * radiusX)) *
|
||||
Math.sqrt(
|
||||
4 /
|
||||
((curX - destX) ** 2 / radiusX ** 2 +
|
||||
(curY - destY) ** 2 / radiusY ** 2) -
|
||||
1,
|
||||
),
|
||||
],
|
||||
[
|
||||
(curX + destX) / 2 -
|
||||
((radiusX * (curY - destY)) / (2 * radiusY)) *
|
||||
Math.sqrt(
|
||||
4 /
|
||||
((curX - destX) ** 2 / radiusX ** 2 +
|
||||
(curY - destY) ** 2 / radiusY ** 2) -
|
||||
1,
|
||||
),
|
||||
(curY + destY) / 2 +
|
||||
((radiusY * (curX - destX)) / (2 * radiusX)) *
|
||||
Math.sqrt(
|
||||
4 /
|
||||
((curX - destX) ** 2 / radiusX ** 2 +
|
||||
(curY - destY) ** 2 / radiusY ** 2) -
|
||||
1,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get point of ellipse at given degree
|
||||
*/
|
||||
const getPointAtDegree = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radiusX: number,
|
||||
radiusY: number,
|
||||
degree: number,
|
||||
): number[] => [
|
||||
Math.round(radiusX * Math.cos(degreeToRadian(degree)) + centerX),
|
||||
Math.round(radiusY * Math.sin(degreeToRadian(degree)) + centerY),
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all points of a given ellipse
|
||||
*/
|
||||
export const getEllipsePoints = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radiusX: number,
|
||||
radiusY: number,
|
||||
): number[][] => {
|
||||
const points: number[][] = [];
|
||||
|
||||
for (let i = 0; i < 360; i += 1) {
|
||||
const pointAtDegree = getPointAtDegree(
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
i,
|
||||
);
|
||||
const existingPoint = points.find(
|
||||
([x, y]) => x === pointAtDegree[0] && y === pointAtDegree[1],
|
||||
);
|
||||
|
||||
if (!existingPoint) {
|
||||
points.push(pointAtDegree);
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find ellipse arc given sweep parameter
|
||||
*/
|
||||
export const findArc = (
|
||||
points: number[][],
|
||||
sweep: boolean,
|
||||
curX: number,
|
||||
curY: number,
|
||||
destX: number,
|
||||
destY: number,
|
||||
): number[][] => {
|
||||
const indexCur = points.findIndex(
|
||||
([x, y]) => x === Math.round(curX) && y === Math.round(curY),
|
||||
);
|
||||
const indexDest = points.findIndex(
|
||||
([x, y]) => x === Math.round(destX) && y === Math.round(destY),
|
||||
);
|
||||
const arc = [];
|
||||
const step = sweep ? -1 : 1;
|
||||
|
||||
for (let i = indexDest; true; i += step) {
|
||||
arc.push(points[i]);
|
||||
|
||||
if (i === indexCur) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (sweep && i === 0) {
|
||||
i = points.length;
|
||||
} else if (!sweep && i === points.length - 1) {
|
||||
i = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return arc.reverse();
|
||||
};
|
||||
313
src/svgToExcalidraw/elements/path/utils/path-to-points.ts
Normal file
313
src/svgToExcalidraw/elements/path/utils/path-to-points.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { PathCommand } from "../../../types";
|
||||
import { safeNumber } from "../../../utils";
|
||||
import { curveToPoints } from "./bezier";
|
||||
import { findArc, getEllipsePoints, getEllipsesCenter } from "./ellipse";
|
||||
|
||||
const PATH_COMMANDS_REGEX =
|
||||
/(?:([HhVv] *-?\d*(?:\.\d+)?)|([MmLlTt](?: *-?\d*(?:\.\d+)?(?:,| *)?){2})|([Cc](?: *-?\d*(?:\.\d+)?(?:,| *)?){6})|([QqSs](?: *-?\d*(?:\.\d+)?(?:,| *)?){4})|([Aa](?: *-?\d*(?:\.\d+)?(?:,| *)?){7})|(z|Z))/g;
|
||||
const COMMAND_REGEX = /(?:[MmLlHhVvCcSsQqTtAaZz]|(-?\d+(?:\.\d+)?))/g;
|
||||
|
||||
const handleMoveToAndLineTo = (
|
||||
currentPosition: number[],
|
||||
parameters: number[],
|
||||
isRelative: boolean,
|
||||
): number[] => {
|
||||
if (isRelative) {
|
||||
return [
|
||||
currentPosition[0] + parameters[0],
|
||||
currentPosition[1] + parameters[1],
|
||||
];
|
||||
}
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
const handleHorizontalLineTo = (
|
||||
currentPosition: number[],
|
||||
x: number,
|
||||
isRelative: boolean,
|
||||
): number[] => {
|
||||
if (isRelative) {
|
||||
return [currentPosition[0] + x, currentPosition[1]];
|
||||
}
|
||||
|
||||
return [x, currentPosition[1]];
|
||||
};
|
||||
|
||||
const handleVerticalLineTo = (
|
||||
currentPosition: number[],
|
||||
y: number,
|
||||
isRelative: boolean,
|
||||
): number[] => {
|
||||
if (isRelative) {
|
||||
return [currentPosition[0], currentPosition[1] + y];
|
||||
}
|
||||
|
||||
return [currentPosition[0], y];
|
||||
};
|
||||
|
||||
const handleCubicCurveTo = (
|
||||
currentPosition: number[],
|
||||
parameters: number[],
|
||||
lastCommand: PathCommand,
|
||||
isSimpleForm: boolean,
|
||||
isRelative: boolean,
|
||||
): number[][] => {
|
||||
const controlPoints = [currentPosition];
|
||||
let inferredControlPoint;
|
||||
|
||||
if (isSimpleForm) {
|
||||
inferredControlPoint = ["C", "c"].includes(lastCommand?.type)
|
||||
? [
|
||||
currentPosition[0] - (lastCommand.parameters[2] - currentPosition[0]),
|
||||
currentPosition[1] - (lastCommand.parameters[3] - currentPosition[1]),
|
||||
]
|
||||
: currentPosition;
|
||||
}
|
||||
|
||||
if (isRelative) {
|
||||
controlPoints.push(
|
||||
inferredControlPoint || [
|
||||
currentPosition[0] + parameters[0],
|
||||
currentPosition[1] + parameters[1],
|
||||
],
|
||||
[currentPosition[0] + parameters[2], currentPosition[1] + parameters[3]],
|
||||
[currentPosition[0] + parameters[4], currentPosition[1] + parameters[5]],
|
||||
);
|
||||
} else {
|
||||
controlPoints.push(
|
||||
inferredControlPoint || [parameters[0], parameters[1]],
|
||||
[parameters[2], parameters[3]],
|
||||
[parameters[4], parameters[5]],
|
||||
);
|
||||
}
|
||||
|
||||
return curveToPoints("cubic", controlPoints);
|
||||
};
|
||||
|
||||
const handleQuadraticCurveTo = (
|
||||
currentPosition: number[],
|
||||
parameters: number[],
|
||||
lastCommand: PathCommand,
|
||||
isSimpleForm: boolean,
|
||||
isRelative: boolean,
|
||||
): number[][] => {
|
||||
const controlPoints = [currentPosition];
|
||||
let inferredControlPoint;
|
||||
|
||||
if (isSimpleForm) {
|
||||
inferredControlPoint = ["Q", "q"].includes(lastCommand?.type)
|
||||
? [
|
||||
currentPosition[0] - (lastCommand.parameters[0] - currentPosition[0]),
|
||||
currentPosition[1] - (lastCommand.parameters[1] - currentPosition[1]),
|
||||
]
|
||||
: currentPosition;
|
||||
}
|
||||
|
||||
if (isRelative) {
|
||||
controlPoints.push(
|
||||
inferredControlPoint || [
|
||||
currentPosition[0] + parameters[0],
|
||||
currentPosition[1] + parameters[1],
|
||||
],
|
||||
[currentPosition[0] + parameters[2], currentPosition[1] + parameters[3]],
|
||||
);
|
||||
} else {
|
||||
controlPoints.push(inferredControlPoint || [parameters[0], parameters[1]], [
|
||||
parameters[2],
|
||||
parameters[3],
|
||||
]);
|
||||
}
|
||||
|
||||
return curveToPoints("quadratic", controlPoints);
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo handle arcs rotation
|
||||
* @todo handle specific cases where only one ellipse can exist
|
||||
*/
|
||||
const handleArcTo = (
|
||||
currentPosition: number[],
|
||||
[radiusX, radiusY, , large, sweep, destX, destY]: number[],
|
||||
isRelative: boolean,
|
||||
): number[][] => {
|
||||
destX = isRelative ? currentPosition[0] + destX : destX;
|
||||
destY = isRelative ? currentPosition[1] + destY : destY;
|
||||
|
||||
const ellipsesCenter = getEllipsesCenter(
|
||||
currentPosition[0],
|
||||
currentPosition[1],
|
||||
destX,
|
||||
destY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
);
|
||||
|
||||
const ellipsesPoints = [
|
||||
getEllipsePoints(
|
||||
ellipsesCenter[0][0],
|
||||
ellipsesCenter[0][1],
|
||||
radiusX,
|
||||
radiusY,
|
||||
),
|
||||
getEllipsePoints(
|
||||
ellipsesCenter[1][0],
|
||||
ellipsesCenter[1][1],
|
||||
radiusX,
|
||||
radiusY,
|
||||
),
|
||||
];
|
||||
|
||||
const arcs = [
|
||||
findArc(
|
||||
ellipsesPoints[0],
|
||||
!!sweep,
|
||||
currentPosition[0],
|
||||
currentPosition[1],
|
||||
destX,
|
||||
destY,
|
||||
),
|
||||
findArc(
|
||||
ellipsesPoints[1],
|
||||
!!sweep,
|
||||
currentPosition[0],
|
||||
currentPosition[1],
|
||||
destX,
|
||||
destY,
|
||||
),
|
||||
];
|
||||
|
||||
const finalArc = arcs.reduce(
|
||||
(arc, curArc) =>
|
||||
(large && curArc.length > arc.length) ||
|
||||
(!large && (!arc.length || curArc.length < arc.length))
|
||||
? curArc
|
||||
: arc,
|
||||
[],
|
||||
);
|
||||
|
||||
return finalArc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a SVG path data to list of points
|
||||
*/
|
||||
const pathToPoints = (path: string): number[][][] => {
|
||||
const commands = path.match(PATH_COMMANDS_REGEX);
|
||||
const elements = [];
|
||||
const commandsHistory = [];
|
||||
let currentPosition = [0, 0];
|
||||
let points = [];
|
||||
|
||||
if (!commands?.length) {
|
||||
throw new Error("No commands found in given path");
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
const lastCommand = commandsHistory[commandsHistory.length - 2];
|
||||
const commandMatch = command.match(COMMAND_REGEX);
|
||||
|
||||
currentPosition = points[points.length - 1] || currentPosition;
|
||||
|
||||
if (commandMatch?.length) {
|
||||
const commandType = commandMatch[0];
|
||||
const parameters = commandMatch
|
||||
.slice(1, commandMatch.length)
|
||||
.map((parameter) => safeNumber(Number(parameter)));
|
||||
const isRelative = commandType.toLowerCase() === commandType;
|
||||
|
||||
commandsHistory.push({
|
||||
type: commandType,
|
||||
parameters,
|
||||
isRelative,
|
||||
});
|
||||
|
||||
switch (commandType) {
|
||||
case "M":
|
||||
case "m":
|
||||
case "L":
|
||||
case "l":
|
||||
points.push(
|
||||
handleMoveToAndLineTo(currentPosition, parameters, isRelative),
|
||||
);
|
||||
|
||||
break;
|
||||
case "H":
|
||||
case "h":
|
||||
points.push(
|
||||
handleHorizontalLineTo(currentPosition, parameters[0], isRelative),
|
||||
);
|
||||
|
||||
break;
|
||||
case "V":
|
||||
case "v":
|
||||
points.push(
|
||||
handleVerticalLineTo(currentPosition, parameters[0], isRelative),
|
||||
);
|
||||
|
||||
break;
|
||||
case "C":
|
||||
case "c":
|
||||
case "S":
|
||||
case "s":
|
||||
points.push(
|
||||
...handleCubicCurveTo(
|
||||
currentPosition,
|
||||
parameters,
|
||||
lastCommand,
|
||||
["S", "s"].includes(commandType),
|
||||
isRelative,
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
case "Q":
|
||||
case "q":
|
||||
case "T":
|
||||
case "t":
|
||||
points.push(
|
||||
...handleQuadraticCurveTo(
|
||||
currentPosition,
|
||||
parameters,
|
||||
lastCommand,
|
||||
["T", "t"].includes(commandType),
|
||||
isRelative,
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
case "A":
|
||||
case "a":
|
||||
points.push(...handleArcTo(currentPosition, parameters, isRelative));
|
||||
|
||||
break;
|
||||
case "Z":
|
||||
case "z":
|
||||
if (points.length) {
|
||||
if (
|
||||
currentPosition[0] !== points[0][0] ||
|
||||
currentPosition[1] !== points[0][1]
|
||||
) {
|
||||
points.push(points[0]);
|
||||
}
|
||||
|
||||
elements.push(points);
|
||||
}
|
||||
|
||||
points = [];
|
||||
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// console.error("Unsupported command provided will be ignored:", command);
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.length === 0 && points.length) {
|
||||
elements.push(points);
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
export default pathToPoints;
|
||||
39
src/svgToExcalidraw/elements/utils.ts
Normal file
39
src/svgToExcalidraw/elements/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ElementBoundaries } from "../types";
|
||||
|
||||
export const getElementBoundaries = (points: number[][]): ElementBoundaries => {
|
||||
const { x, y } = points.reduce(
|
||||
(boundaries, [x, y]) => {
|
||||
if (x < boundaries.x.min) {
|
||||
boundaries.x.min = x;
|
||||
}
|
||||
if (x > boundaries.x.max) {
|
||||
boundaries.x.max = x;
|
||||
}
|
||||
if (y < boundaries.y.min) {
|
||||
boundaries.y.min = y;
|
||||
}
|
||||
if (y > boundaries.y.max) {
|
||||
boundaries.y.max = y;
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
},
|
||||
{
|
||||
x: {
|
||||
min: Infinity,
|
||||
max: 0,
|
||||
},
|
||||
y: {
|
||||
min: Infinity,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
x: x.min,
|
||||
y: y.min,
|
||||
width: x.max - x.min,
|
||||
height: y.max - y.min,
|
||||
};
|
||||
};
|
||||
40
src/svgToExcalidraw/parser.ts
Normal file
40
src/svgToExcalidraw/parser.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import ExcalidrawScene from "./elements/ExcalidrawScene";
|
||||
import Group from "./elements/Group";
|
||||
import { createTreeWalker, walk } from "./walker";
|
||||
|
||||
export type ConversionResult = {
|
||||
hasErrors: boolean;
|
||||
errors: NodeListOf<Element> | null;
|
||||
content: any; // Serialized Excalidraw JSON
|
||||
};
|
||||
|
||||
export const svgToExcalidraw = (svgString: string): ConversionResult => {
|
||||
const parser = new DOMParser();
|
||||
const svgDOM = parser.parseFromString(svgString, "image/svg+xml");
|
||||
|
||||
// was there a parsing error?
|
||||
const errorsElements = svgDOM.querySelectorAll("parsererror");
|
||||
const hasErrors = errorsElements.length > 0;
|
||||
let content = null;
|
||||
|
||||
if (hasErrors) {
|
||||
console.error(
|
||||
"There were errors while parsing the given SVG: ",
|
||||
[...errorsElements].map((el) => el.innerHTML),
|
||||
);
|
||||
} else {
|
||||
const tw = createTreeWalker(svgDOM);
|
||||
const scene = new ExcalidrawScene();
|
||||
const groups: Group[] = [];
|
||||
|
||||
walk({ tw, scene, groups, root: svgDOM }, tw.nextNode());
|
||||
|
||||
content = scene.elements; //scene.toExJSON();
|
||||
}
|
||||
|
||||
return {
|
||||
hasErrors,
|
||||
errors: hasErrors ? errorsElements : null,
|
||||
content,
|
||||
};
|
||||
};
|
||||
2
src/svgToExcalidraw/readme.md
Normal file
2
src/svgToExcalidraw/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Original source https://github.com/excalidraw/svg-to-excalidraw. Last commit: https://github.com/excalidraw/svg-to-excalidraw/commit/6f6e4b7269c4194b56cf7517a8357ba73be12a3a
|
||||
Embedded into the project instead of using an import because compiled file size difference (smaller this way). Also the svg-to-excalidraw package has not been maintained for over a year, thus I don't expect to miss out on frequent updates
|
||||
173
src/svgToExcalidraw/transform.ts
Normal file
173
src/svgToExcalidraw/transform.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import Group from "./elements/Group";
|
||||
import { vec3, mat4 } from "gl-matrix";
|
||||
|
||||
/*
|
||||
SVG transform attr is a bit strange in that it can accept traditional
|
||||
css transform string (at least per spec) as well as a it's own "unitless"
|
||||
version of transform functions.
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
|
||||
*/
|
||||
|
||||
const transformFunctions = {
|
||||
matrix: "matrix",
|
||||
matrix3d: "matrix3d",
|
||||
perspective: "perspective",
|
||||
rotate: "rotate",
|
||||
rotate3d: "rotate3d",
|
||||
rotateX: "rotateX",
|
||||
rotateY: "rotateY",
|
||||
rotateZ: "rotateZ",
|
||||
scale: "scale",
|
||||
scale3d: "scale3d",
|
||||
scaleX: "scaleX",
|
||||
scaleY: "scaleY",
|
||||
scaleZ: "scaleZ",
|
||||
skew: "skew",
|
||||
skewX: "skewX",
|
||||
skewY: "skewY",
|
||||
translate: "translate",
|
||||
translate3d: "translate3d",
|
||||
translateX: "translateX",
|
||||
translateY: "translateY",
|
||||
translateZ: "translateZ",
|
||||
} as const;
|
||||
|
||||
const transformFunctionsArr = Object.keys(transformFunctions);
|
||||
|
||||
// type Transform
|
||||
|
||||
type TransformFuncValue = {
|
||||
value: string;
|
||||
unit: string;
|
||||
};
|
||||
|
||||
type TransformFunc = {
|
||||
type: keyof typeof transformFunctions;
|
||||
values: TransformFuncValue[];
|
||||
};
|
||||
|
||||
const defaultUnits = {
|
||||
matrix: "",
|
||||
matrix3d: "",
|
||||
perspective: "perspective",
|
||||
rotate: "deg",
|
||||
rotate3d: "deg",
|
||||
rotateX: "deg",
|
||||
rotateY: "deg",
|
||||
rotateZ: "deg",
|
||||
scale: "",
|
||||
scale3d: "",
|
||||
scaleX: "",
|
||||
scaleY: "",
|
||||
scaleZ: "",
|
||||
skew: "skew",
|
||||
skewX: "deg",
|
||||
skewY: "deg",
|
||||
translate: "px",
|
||||
translate3d: "px",
|
||||
translateX: "px",
|
||||
translateY: "px",
|
||||
translateZ: "px",
|
||||
};
|
||||
|
||||
// Convert between possible svg transform attribute values to css transform attribute values.
|
||||
const svgTransformToCSSTransform = (svgTransformStr: string): string => {
|
||||
// Create transform function string "chunks", e.g "rotate(90deg)"
|
||||
const tFuncs = svgTransformStr.match(/(\w+)\(([^)]*)\)/g);
|
||||
if (!tFuncs) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const tFuncValues: TransformFunc[] = tFuncs.map((tFuncStr): TransformFunc => {
|
||||
const type = tFuncStr.split("(")[0] as keyof typeof transformFunctions;
|
||||
if (!type) {
|
||||
throw new Error("Unable to find transform name");
|
||||
}
|
||||
if (!transformFunctionsArr.includes(type)) {
|
||||
throw new Error(`transform function name "${type}" is not valid`);
|
||||
}
|
||||
|
||||
// get the arg/props of the transform function, e.g "90deg".
|
||||
const tFuncParts = tFuncStr.match(/([-+]?[0-9]*\.?[0-9]+)([a-z])*/g);
|
||||
if (!tFuncParts) {
|
||||
return { type, values: [] };
|
||||
}
|
||||
|
||||
let values = tFuncParts.map((a): TransformFuncValue => {
|
||||
// Separate the arg value and unit. e.g ["90", "deg"]
|
||||
const [value, unit] = a.matchAll(/([-+]?[0-9]*\.?[0-9]+)|([a-z])*/g);
|
||||
|
||||
return {
|
||||
unit: unit[0] || defaultUnits[type],
|
||||
value: value[0],
|
||||
};
|
||||
});
|
||||
|
||||
// Not supporting x, y args of svg rotate transform yet...
|
||||
if (values && type === "rotate" && values?.length > 1) {
|
||||
values = [values[0]];
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
values,
|
||||
};
|
||||
});
|
||||
|
||||
// Generate a string of transform functions that can be set as a CSS Transform.
|
||||
const csstransformStr = tFuncValues
|
||||
.map(({ type, values }) => {
|
||||
const valStr = values
|
||||
.map(({ unit, value }) => `${value}${unit}`)
|
||||
.join(", ");
|
||||
return `${type}(${valStr})`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return csstransformStr;
|
||||
};
|
||||
|
||||
export const createDOMMatrixFromSVGStr = (
|
||||
svgTransformStr: string,
|
||||
): DOMMatrix => {
|
||||
const cssTransformStr = svgTransformToCSSTransform(svgTransformStr);
|
||||
|
||||
return new DOMMatrix(cssTransformStr);
|
||||
};
|
||||
|
||||
export function getElementMatrix(el: Element): mat4 {
|
||||
if (el.hasAttribute("transform")) {
|
||||
const elMat = new DOMMatrix(
|
||||
svgTransformToCSSTransform(el.getAttribute("transform") || ""),
|
||||
);
|
||||
|
||||
return mat4.multiply(mat4.create(), mat4.create(), elMat.toFloat32Array());
|
||||
}
|
||||
|
||||
return mat4.create();
|
||||
}
|
||||
|
||||
export function getTransformMatrix(el: Element, groups: Group[]): mat4 {
|
||||
const accumMat = groups
|
||||
.map(({ element }) => getElementMatrix(element))
|
||||
.concat([getElementMatrix(el)])
|
||||
.reduce((acc, mat) => mat4.multiply(acc, acc, mat), mat4.create());
|
||||
|
||||
return accumMat;
|
||||
}
|
||||
|
||||
export function transformPoints(
|
||||
points: number[][],
|
||||
transform: mat4,
|
||||
): [number, number][] {
|
||||
return points.map(([x, y]) => {
|
||||
const [newX, newY] = vec3.transformMat4(
|
||||
vec3.create(),
|
||||
vec3.fromValues(x, y, 1),
|
||||
transform,
|
||||
);
|
||||
|
||||
return [newX, newY];
|
||||
});
|
||||
}
|
||||
118
src/svgToExcalidraw/types.ts
Normal file
118
src/svgToExcalidraw/types.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FillStyle, GroupId, StrokeSharpness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
|
||||
export type PathCommand = {
|
||||
type: string;
|
||||
parameters: number[];
|
||||
isRelative: boolean;
|
||||
};
|
||||
|
||||
export type RawElement = {
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
points: number[][];
|
||||
backgroundColor: string;
|
||||
strokeColor: string;
|
||||
};
|
||||
|
||||
export type ElementBoundaries = {
|
||||
x: number;
|
||||
y: number;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
/* from Excalidraw codebase */
|
||||
|
||||
// 1-based in case we ever do `if(element.fontFamily)`
|
||||
export const FONT_FAMILY = {
|
||||
1: "Virgil",
|
||||
2: "Helvetica",
|
||||
3: "Cascadia",
|
||||
} as const;
|
||||
|
||||
export declare type RoughPoint = [number, number];
|
||||
export type Point = Readonly<RoughPoint>;
|
||||
|
||||
export declare type Line = [Point, Point];
|
||||
export interface Rectangle {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type _ExcalidrawElementBase = Readonly<{
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
width: number;
|
||||
height: number;
|
||||
angle: number;
|
||||
/** Random integer used to seed shape generation so that the roughjs shape
|
||||
doesn't differ across renders. */
|
||||
seed: number;
|
||||
/** Integer that is sequentially incremented on each change. Used to reconcile
|
||||
elements during collaboration or when saving to server. */
|
||||
version: number;
|
||||
/** Random integer that is regenerated on each change.
|
||||
Used for deterministic reconciliation of updates during collaboration,
|
||||
in case the versions (see above) are identical. */
|
||||
versionNonce: number;
|
||||
isDeleted: boolean;
|
||||
/** List of groups the element belongs to.
|
||||
Ordered from deepest to shallowest. */
|
||||
groupIds: readonly GroupId[];
|
||||
/** Ids of (linear) elements that are bound to this element. */
|
||||
boundElementIds: readonly ExcalidrawLinearElement["id"][] | null;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||
type: "selection";
|
||||
};
|
||||
|
||||
export type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
|
||||
type: "rectangle";
|
||||
};
|
||||
|
||||
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
||||
type: "diamond";
|
||||
};
|
||||
|
||||
export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
||||
type: "ellipse";
|
||||
};
|
||||
|
||||
/**
|
||||
* These are elements that don't have any additional properties.
|
||||
*/
|
||||
export type ExcalidrawGenericElement =
|
||||
| ExcalidrawSelectionElement
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement;
|
||||
|
||||
/**
|
||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||
* no computed data. The list of all ExcalidrawElements should be shareable
|
||||
* between peers and contain no state local to the peer.
|
||||
*/
|
||||
export type _ExcalidrawElement =
|
||||
| ExcalidrawGenericElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
isDeleted: false;
|
||||
};
|
||||
|
||||
40
src/svgToExcalidraw/utils.ts
Normal file
40
src/svgToExcalidraw/utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Point } from "./elements/ExcalidrawElement";
|
||||
|
||||
const random = new Random(Date.now());
|
||||
|
||||
export const randomInteger = (): number => Math.floor(random.next() * 2 ** 31);
|
||||
|
||||
export const randomId = (): string => nanoid();
|
||||
|
||||
export const safeNumber = (number: number): number => Number(number.toFixed(2));
|
||||
|
||||
export function dimensionsFromPoints(points: number[][]): number[] {
|
||||
const xCoords = points.map(([x]) => x);
|
||||
const yCoords = points.map(([, y]) => y);
|
||||
|
||||
const minX = Math.min(...xCoords);
|
||||
const minY = Math.min(...yCoords);
|
||||
const maxX = Math.max(...xCoords);
|
||||
const maxY = Math.max(...yCoords);
|
||||
|
||||
return [maxX - minX, maxY - minY];
|
||||
}
|
||||
|
||||
// winding order is clockwise values is positive, counter clockwise if negative.
|
||||
export function getWindingOrder(
|
||||
points: Point[],
|
||||
): "clockwise" | "counterclockwise" {
|
||||
const total = points.reduce((acc, [x1, y1], idx, arr) => {
|
||||
const p2 = arr[idx + 1];
|
||||
const x2 = p2 ? p2[0] : 0;
|
||||
const y2 = p2 ? p2[1] : 0;
|
||||
|
||||
const e = (x2 - x1) * (y2 + y1);
|
||||
|
||||
return e + acc;
|
||||
}, 0);
|
||||
|
||||
return total > 0 ? "clockwise" : "counterclockwise";
|
||||
}
|
||||
463
src/svgToExcalidraw/walker.ts
Normal file
463
src/svgToExcalidraw/walker.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
import { mat4 } from "gl-matrix";
|
||||
import { dimensionsFromPoints } from "./utils";
|
||||
import ExcalidrawScene from "./elements/ExcalidrawScene";
|
||||
import Group, { getGroupAttrs } from "./elements/Group";
|
||||
import {
|
||||
ExcalidrawElementBase,
|
||||
ExcalidrawRectangle,
|
||||
ExcalidrawEllipse,
|
||||
ExcalidrawLine,
|
||||
ExcalidrawDraw,
|
||||
createExRect,
|
||||
createExEllipse,
|
||||
createExLine,
|
||||
createExDraw,
|
||||
Point,
|
||||
} from "./elements/ExcalidrawElement";
|
||||
import {
|
||||
presAttrsToElementValues,
|
||||
filterAttrsToElementValues,
|
||||
pointsAttrToPoints,
|
||||
has,
|
||||
get,
|
||||
getNum,
|
||||
} from "./attributes";
|
||||
import { getTransformMatrix, transformPoints } from "./transform";
|
||||
import { pointsOnPath } from "points-on-path";
|
||||
import { randomId, getWindingOrder } from "./utils";
|
||||
|
||||
const SUPPORTED_TAGS = [
|
||||
"svg",
|
||||
"path",
|
||||
"g",
|
||||
"use",
|
||||
"circle",
|
||||
"ellipse",
|
||||
"rect",
|
||||
"polyline",
|
||||
"polygon",
|
||||
];
|
||||
|
||||
const nodeValidator = (node: Element): number => {
|
||||
if (SUPPORTED_TAGS.includes(node.tagName)) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
};
|
||||
|
||||
export function createTreeWalker(dom: Node): TreeWalker {
|
||||
return document.createTreeWalker(dom, NodeFilter.SHOW_ALL, {
|
||||
acceptNode: nodeValidator,
|
||||
});
|
||||
}
|
||||
|
||||
type WalkerArgs = {
|
||||
root: Document;
|
||||
tw: TreeWalker;
|
||||
scene: ExcalidrawScene;
|
||||
groups: Group[];
|
||||
};
|
||||
|
||||
const presAttrs = (
|
||||
el: Element,
|
||||
groups: Group[],
|
||||
): Partial<ExcalidrawElementBase> => {
|
||||
return {
|
||||
...getGroupAttrs(groups),
|
||||
...presAttrsToElementValues(el),
|
||||
...filterAttrsToElementValues(el),
|
||||
};
|
||||
};
|
||||
|
||||
const skippedUseAttrs = ["id"];
|
||||
const allwaysPassedUseAttrs = [
|
||||
"x",
|
||||
"y",
|
||||
"width",
|
||||
"height",
|
||||
"href",
|
||||
"xlink:href",
|
||||
];
|
||||
|
||||
/*
|
||||
"Most attributes on use do not override those already on the element
|
||||
referenced by use. (This differs from how CSS style attributes override
|
||||
those set 'earlier' in the cascade). Only the attributes x, y, width,
|
||||
height and href on the use element will override those set on the
|
||||
referenced element. However, any other attributes not set on the referenced
|
||||
element will be applied to the use element."
|
||||
|
||||
Situation 1: Attr is set on defEl, NOT on useEl
|
||||
- result: use defEl attr
|
||||
Situation 2: Attr is on useEl, NOT on defEl
|
||||
- result: use the useEl attr
|
||||
Situation 3: Attr is on both useEl and defEl
|
||||
- result: use the defEl attr (Unless x, y, width, height, href, xlink:href)
|
||||
*/
|
||||
const getDefElWithCorrectAttrs = (defEl: Element, useEl: Element): Element => {
|
||||
const finalEl = [...useEl.attributes].reduce((el, attr) => {
|
||||
if (skippedUseAttrs.includes(attr.value)) {
|
||||
return el;
|
||||
}
|
||||
|
||||
// Does defEl have the attr? If so, use it, else use the useEl attr
|
||||
if (
|
||||
!defEl.hasAttribute(attr.name) ||
|
||||
allwaysPassedUseAttrs.includes(attr.name)
|
||||
) {
|
||||
el.setAttribute(attr.name, useEl.getAttribute(attr.name) || "");
|
||||
}
|
||||
return el;
|
||||
}, defEl.cloneNode() as Element);
|
||||
|
||||
return finalEl;
|
||||
};
|
||||
|
||||
const walkers = {
|
||||
svg: (args: WalkerArgs) => {
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
g: (args: WalkerArgs) => {
|
||||
const nextArgs = {
|
||||
...args,
|
||||
tw: createTreeWalker(args.tw.currentNode),
|
||||
groups: [...args.groups, new Group(args.tw.currentNode as Element)],
|
||||
};
|
||||
|
||||
walk(nextArgs, nextArgs.tw.nextNode());
|
||||
|
||||
walk(args, args.tw.nextSibling());
|
||||
},
|
||||
|
||||
use: (args: WalkerArgs) => {
|
||||
const { root, tw, scene } = args;
|
||||
const useEl = tw.currentNode as Element;
|
||||
|
||||
const id = useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
|
||||
|
||||
if (!id) {
|
||||
throw new Error("unable to get id of use element");
|
||||
}
|
||||
|
||||
const defEl = root.querySelector(id);
|
||||
|
||||
if (!defEl) {
|
||||
throw new Error(`unable to find def element with id: ${id}`);
|
||||
}
|
||||
|
||||
const tempScene = new ExcalidrawScene();
|
||||
|
||||
const finalEl = getDefElWithCorrectAttrs(defEl, useEl);
|
||||
|
||||
walk(
|
||||
{
|
||||
...args,
|
||||
scene: tempScene,
|
||||
tw: createTreeWalker(finalEl),
|
||||
},
|
||||
finalEl,
|
||||
);
|
||||
|
||||
const exEl = tempScene.elements.pop();
|
||||
|
||||
if (exEl) {
|
||||
scene.elements.push(exEl);
|
||||
//throw new Error("Unable to create ex element");
|
||||
}
|
||||
|
||||
|
||||
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
circle: (args: WalkerArgs): void => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const r = getNum(el, "r", 0);
|
||||
const d = r * 2;
|
||||
const x = getNum(el, "x", 0) + getNum(el, "cx", 0) - r;
|
||||
const y = getNum(el, "y", 0) + getNum(el, "cy", 0) - r;
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
// @ts-ignore
|
||||
const m = mat4.fromValues(d, 0, 0, 0, 0, d, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||
|
||||
const result = mat4.multiply(mat4.create(), mat, m);
|
||||
|
||||
const circle: ExcalidrawEllipse = {
|
||||
...createExEllipse(),
|
||||
...presAttrs(el, groups),
|
||||
x: result[12],
|
||||
y: result[13],
|
||||
width: result[0],
|
||||
height: result[5],
|
||||
groupIds: groups.map((g) => g.id),
|
||||
};
|
||||
|
||||
scene.elements.push(circle);
|
||||
|
||||
walk(args, tw.nextNode());
|
||||
},
|
||||
|
||||
ellipse: (args: WalkerArgs): void => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const rx = getNum(el, "rx", 0);
|
||||
const ry = getNum(el, "ry", 0);
|
||||
const cx = getNum(el, "cx", 0);
|
||||
const cy = getNum(el, "cy", 0);
|
||||
const x = getNum(el, "x", 0) + cx - rx;
|
||||
const y = getNum(el, "y", 0) + cy - ry;
|
||||
const w = rx * 2;
|
||||
const h = ry * 2;
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||
|
||||
const result = mat4.multiply(mat4.create(), mat, m);
|
||||
|
||||
const ellipse: ExcalidrawEllipse = {
|
||||
...createExEllipse(),
|
||||
...presAttrs(el, groups),
|
||||
x: result[12],
|
||||
y: result[13],
|
||||
width: result[0],
|
||||
height: result[5],
|
||||
groupIds: groups.map((g) => g.id),
|
||||
};
|
||||
|
||||
scene.elements.push(ellipse);
|
||||
|
||||
walk(args, tw.nextNode());
|
||||
},
|
||||
|
||||
line: (args: WalkerArgs) => {
|
||||
// unimplemented
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
polygon: (args: WalkerArgs) => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const points = pointsAttrToPoints(el);
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
const transformedPoints = transformPoints(points, mat);
|
||||
|
||||
// The first point needs to be 0, 0, and all following points
|
||||
// are relative to the first point.
|
||||
const x = transformedPoints[0][0];
|
||||
const y = transformedPoints[0][1];
|
||||
|
||||
const relativePoints = transformedPoints.map(([_x, _y]) => [
|
||||
_x - x,
|
||||
_y - y,
|
||||
]);
|
||||
|
||||
const [width, height] = dimensionsFromPoints(relativePoints);
|
||||
|
||||
const line: ExcalidrawLine = {
|
||||
...createExLine(),
|
||||
...getGroupAttrs(groups),
|
||||
...presAttrsToElementValues(el),
|
||||
points: relativePoints.concat([[0, 0]]),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
scene.elements.push(line);
|
||||
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
polyline: (args: WalkerArgs) => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
const points = pointsAttrToPoints(el);
|
||||
const transformedPoints = transformPoints(points, mat);
|
||||
|
||||
// The first point needs to be 0, 0, and all following points
|
||||
// are relative to the first point.
|
||||
const x = transformedPoints[0][0];
|
||||
const y = transformedPoints[0][1];
|
||||
|
||||
const relativePoints = transformedPoints.map(([_x, _y]) => [
|
||||
_x - x,
|
||||
_y - y,
|
||||
]);
|
||||
|
||||
const [width, height] = dimensionsFromPoints(relativePoints);
|
||||
|
||||
const hasFill = has(el, "fill");
|
||||
const fill = get(el, "fill");
|
||||
|
||||
const shouldFill = !hasFill || (hasFill && fill !== "none");
|
||||
|
||||
const line: ExcalidrawLine = {
|
||||
...createExLine(),
|
||||
...getGroupAttrs(groups),
|
||||
...presAttrsToElementValues(el),
|
||||
points: relativePoints.concat(shouldFill ? [[0, 0]] : []),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
scene.elements.push(line);
|
||||
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
rect: (args: WalkerArgs) => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const x = getNum(el, "x", 0);
|
||||
const y = getNum(el, "y", 0);
|
||||
const w = getNum(el, "width", 0);
|
||||
const h = getNum(el, "height", 0);
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
// @ts-ignore
|
||||
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
||||
|
||||
const result = mat4.multiply(mat4.create(), mat, m);
|
||||
|
||||
/*
|
||||
NOTE: Currently there doesn't seem to be a way to specify the border
|
||||
radius of a rect within Excalidraw. This means that attributes
|
||||
rx and ry can't be used.
|
||||
*/
|
||||
const isRound = el.hasAttribute("rx") || el.hasAttribute("ry");
|
||||
|
||||
const rect: ExcalidrawRectangle = {
|
||||
...createExRect(),
|
||||
...presAttrs(el, groups),
|
||||
x: result[12],
|
||||
y: result[13],
|
||||
width: result[0],
|
||||
height: result[5],
|
||||
strokeSharpness: isRound ? "round" : "sharp",
|
||||
};
|
||||
|
||||
scene.elements.push(rect);
|
||||
|
||||
walk(args, args.tw.nextNode());
|
||||
},
|
||||
|
||||
path: (args: WalkerArgs) => {
|
||||
const { tw, scene, groups } = args;
|
||||
const el = tw.currentNode as Element;
|
||||
|
||||
const mat = getTransformMatrix(el, groups);
|
||||
|
||||
const points = pointsOnPath(get(el, "d"));
|
||||
|
||||
const fillColor = get(el, "fill", "black");
|
||||
const fillRule = get(el, "fill-rule", "nonzero");
|
||||
|
||||
let elements: ExcalidrawDraw[] = [];
|
||||
let localGroup = randomId();
|
||||
|
||||
switch (fillRule) {
|
||||
case "nonzero":
|
||||
let initialWindingOrder = "clockwise";
|
||||
|
||||
elements = points.map((pointArr, idx): ExcalidrawDraw => {
|
||||
const tPoints: Point[] = transformPoints(pointArr, mat4.clone(mat));
|
||||
const x = tPoints[0][0];
|
||||
const y = tPoints[0][1];
|
||||
|
||||
const [width, height] = dimensionsFromPoints(tPoints);
|
||||
|
||||
const relativePoints = tPoints.map(
|
||||
([_x, _y]): Point => [_x - x, _y - y],
|
||||
);
|
||||
|
||||
const windingOrder = getWindingOrder(relativePoints);
|
||||
if (idx === 0) {
|
||||
initialWindingOrder = windingOrder;
|
||||
localGroup = randomId();
|
||||
}
|
||||
|
||||
let backgroundColor = fillColor;
|
||||
if (initialWindingOrder !== windingOrder) {
|
||||
backgroundColor = "#FFFFFF";
|
||||
}
|
||||
|
||||
return {
|
||||
...createExDraw(),
|
||||
strokeWidth: 0,
|
||||
strokeColor: "#00000000",
|
||||
...presAttrs(el, groups),
|
||||
points: relativePoints,
|
||||
backgroundColor,
|
||||
width,
|
||||
height,
|
||||
x: x + getNum(el, "x", 0),
|
||||
y: y + getNum(el, "y", 0),
|
||||
groupIds: [localGroup],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case "evenodd":
|
||||
elements = points.map((pointArr, idx): ExcalidrawDraw => {
|
||||
const tPoints: Point[] = transformPoints(pointArr, mat4.clone(mat));
|
||||
const x = tPoints[0][0];
|
||||
const y = tPoints[0][1];
|
||||
|
||||
const [width, height] = dimensionsFromPoints(tPoints);
|
||||
|
||||
const relativePoints = tPoints.map(
|
||||
([_x, _y]): Point => [_x - x, _y - y],
|
||||
);
|
||||
|
||||
if (idx === 0) {
|
||||
localGroup = randomId();
|
||||
}
|
||||
|
||||
return {
|
||||
...createExDraw(),
|
||||
...presAttrs(el, groups),
|
||||
points: relativePoints,
|
||||
width,
|
||||
height,
|
||||
x: x + getNum(el, "x", 0),
|
||||
y: y + getNum(el, "y", 0),
|
||||
};
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
scene.elements = scene.elements.concat(elements);
|
||||
|
||||
walk(args, tw.nextNode());
|
||||
},
|
||||
};
|
||||
|
||||
export function walk(args: WalkerArgs, nextNode: Node | null): void {
|
||||
if (!nextNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeName = nextNode.nodeName as keyof typeof walkers;
|
||||
if (walkers[nodeName]) {
|
||||
walkers[nodeName](args);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { compressToBase64, decompressFromBase64 } from "lz-string";
|
||||
import { getIMGFilename } from "./FileUtils";
|
||||
import ExcalidrawScene from "lib/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -85,7 +86,7 @@ const random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
|
||||
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
|
||||
export function wrapText(
|
||||
export function wrapTextAtCharLength(
|
||||
text: string,
|
||||
lineLen: number,
|
||||
forceWrap: boolean = false,
|
||||
@@ -649,3 +650,19 @@ export const awaitNextAnimationFrame = async () => new Promise(requestAnimationF
|
||||
export const log = console.log.bind(window.console);
|
||||
export const debug = console.log.bind(window.console);
|
||||
//export const debug = function(){};
|
||||
|
||||
|
||||
export const getContainerElement = (
|
||||
element:
|
||||
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
|
||||
| null,
|
||||
scene: ExcalidrawScene,
|
||||
) => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.containerId) {
|
||||
return scene.elements.filter(el=>el.id === element.containerId)[0] ?? null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
69
styles.css
69
styles.css
@@ -96,8 +96,7 @@ li[data-testid] {
|
||||
|
||||
.ex-coffee-div {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.excalidraw-scriptengine-install td>img {
|
||||
@@ -184,9 +183,8 @@ li[data-testid] {
|
||||
}
|
||||
|
||||
.excalidraw-release .modal {
|
||||
max-height: 90%;
|
||||
width: auto;
|
||||
max-width: 130ch;
|
||||
max-height: 80%;
|
||||
max-width: 100ch;
|
||||
}
|
||||
|
||||
.excalidraw .Island .scrollbar {
|
||||
@@ -225,6 +223,65 @@ textarea.excalidraw-wysiwyg {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.is-tablet .excalidraw button {
|
||||
.is-tablet .excalidraw button,
|
||||
.is-mobile .excalidraw button {
|
||||
padding: initial;
|
||||
height: 1.8rem;
|
||||
}
|
||||
|
||||
.excalidraw button,
|
||||
.ToolIcon button {
|
||||
box-shadow: none;
|
||||
justify-content: initial;
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
--default-button-size: 2rem !important;
|
||||
--default-icon-size: 1rem !important;
|
||||
--lg-button-size: 1.8rem !important;
|
||||
--lg-icon-size: 1rem !important;
|
||||
}
|
||||
|
||||
.excalidraw .tray-zoom {
|
||||
pointer-events: initial;
|
||||
padding-bottom: 0.05rem;
|
||||
padding-top: 0.05rem;
|
||||
}
|
||||
|
||||
.excalidraw-container.theme--dark {
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* https://discordapp.com/channels/686053708261228577/989603365606531104/1041266507256184863 */
|
||||
/*.workspace-leaf {
|
||||
contain: none !important;
|
||||
}*/
|
||||
|
||||
.color-picker-content {
|
||||
overflow-y: auto;
|
||||
max-height: 10rem;
|
||||
}
|
||||
|
||||
.excalidraw .FixedSideContainer_side_top {
|
||||
top: 0.3rem;
|
||||
}
|
||||
|
||||
.excalidraw .ToolIcon__keybinding {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.Island > .Stack > .Stack {
|
||||
padding:0.2rem;
|
||||
}
|
||||
|
||||
label.color-input-container > input {
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.excalidraw .FixedSideContainer_side_top {
|
||||
left: 10px !important;
|
||||
top: 10px !important;
|
||||
right: 10px !important;
|
||||
bottom: 10px !important;
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@@ -1676,6 +1676,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/chroma-js@^2.1.4":
|
||||
"integrity" "sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ=="
|
||||
"resolved" "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.4.tgz"
|
||||
"version" "2.1.4"
|
||||
|
||||
"@types/codemirror@0.0.108":
|
||||
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
|
||||
"resolved" "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
||||
@@ -2216,10 +2221,10 @@
|
||||
dependencies:
|
||||
"@zerollup/ts-helpers" "^1.7.18"
|
||||
|
||||
"@zsviczian/excalidraw@0.13.0-obsidian":
|
||||
"integrity" "sha512-c4SnBEGKtenLB/1gSjXe3BVA+yZfo8b1p2E7sVcaPG8MTz6cpQsCB2+cv7Zta5ihIxuGfK3ZSepVhMbN7RFY2w=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.13.0-obsidian.tgz"
|
||||
"version" "0.13.0-obsidian"
|
||||
"@zsviczian/excalidraw@0.13.0-obsidian-1":
|
||||
"integrity" "sha512-gHfuEX/qrBa+4kolxEkQ/3W5hGfSLoJSXDpuhb8Mvvyyl148hsuWmhUQGFWcNee73YbuQ0arb3hXqwnMUgK0Ig=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.13.0-obsidian-1.tgz"
|
||||
"version" "0.13.0-obsidian-1"
|
||||
|
||||
"abab@^2.0.3", "abab@^2.0.5":
|
||||
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||
@@ -2964,6 +2969,11 @@
|
||||
optionalDependencies:
|
||||
"fsevents" "~2.3.2"
|
||||
|
||||
"chroma-js@^2.4.2":
|
||||
"integrity" "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
|
||||
"resolved" "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz"
|
||||
"version" "2.4.2"
|
||||
|
||||
"chrome-trace-event@^1.0.2":
|
||||
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
||||
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
|
||||
@@ -4628,6 +4638,11 @@
|
||||
"call-bind" "^1.0.2"
|
||||
"get-intrinsic" "^1.1.1"
|
||||
|
||||
"gl-matrix@^3.4.3":
|
||||
"integrity" "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
"resolved" "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz"
|
||||
"version" "3.4.3"
|
||||
|
||||
"glob-parent@^5.1.2", "glob-parent@~5.1.2":
|
||||
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
|
||||
"resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user