mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 225c6305d1 | |||
| ba9ab61cc9 |
+4
-1
@@ -26,7 +26,9 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"roughjs": "^4.5.2",
|
"roughjs": "^4.5.2",
|
||||||
"colormaster": "1.2.1"
|
"colormaster": "1.2.1",
|
||||||
|
"chroma-js": "^2.4.2",
|
||||||
|
"gl-matrix": "^3.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.12",
|
"@babel/core": "^7.16.12",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"@rollup/plugin-replace": "^3.0.1",
|
"@rollup/plugin-replace": "^3.0.1",
|
||||||
"@rollup/plugin-typescript": "^8.3.0",
|
"@rollup/plugin-typescript": "^8.3.0",
|
||||||
"@types/js-beautify": "^1.13.3",
|
"@types/js-beautify": "^1.13.3",
|
||||||
|
"@types/chroma-js": "^2.1.4",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
} from "@zsviczian/excalidraw/types/element/types";
|
} 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 ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
|
||||||
import { ExcalidrawData } from "./ExcalidrawData";
|
import { ExcalidrawData } from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +58,7 @@ import HSVPlugin from "colormaster/plugins/hsv";
|
|||||||
import RYBPlugin from "colormaster/plugins/ryb";
|
import RYBPlugin from "colormaster/plugins/ryb";
|
||||||
import CMYKPlugin from "colormaster/plugins/cmyk";
|
import CMYKPlugin from "colormaster/plugins/cmyk";
|
||||||
import { TInput } from "colormaster/types";
|
import { TInput } from "colormaster/types";
|
||||||
|
import {ConversionResult, svgToExcalidraw} from "./svgToExcalidraw/parser"
|
||||||
|
|
||||||
extendPlugins([
|
extendPlugins([
|
||||||
HarmonyPlugin,
|
HarmonyPlugin,
|
||||||
@@ -1873,6 +1874,16 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
|||||||
|
|
||||||
return CM(color);
|
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(
|
export async function initExcalidrawAutomate(
|
||||||
|
|||||||
@@ -306,7 +306,6 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (!this.getScene || !this.file) {
|
if (!this.getScene || !this.file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
const prompt = new Prompt(
|
const prompt = new Prompt(
|
||||||
app,
|
app,
|
||||||
@@ -651,7 +650,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||||
this.toolsPanelRef.current.setFullscreen(true);
|
this.toolsPanelRef.current.setFullscreen(true);
|
||||||
}
|
}
|
||||||
if (app.isMobile) {
|
if (this.plugin.device.isPhone) {
|
||||||
if(Platform.isIosApp) {
|
if(Platform.isIosApp) {
|
||||||
this.restoreMobileLeaves();
|
this.restoreMobileLeaves();
|
||||||
app.workspace.getLayout().main.children
|
app.workspace.getLayout().main.children
|
||||||
@@ -735,7 +734,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
||||||
this.toolsPanelRef.current.setFullscreen(false);
|
this.toolsPanelRef.current.setFullscreen(false);
|
||||||
}
|
}
|
||||||
if (app.isMobile) {
|
if (this.plugin.device.isPhone) {
|
||||||
this.restoreMobileLeaves();
|
this.restoreMobileLeaves();
|
||||||
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
||||||
if (oldStylesheet) {
|
if (oldStylesheet) {
|
||||||
@@ -2354,7 +2353,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
elements,
|
elements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
},
|
},
|
||||||
false,
|
true, //set to true because svtToExcalidraw generates a legacy Excalidraw object
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2796,7 +2795,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (this.semaphores.justLoaded) {
|
if (this.semaphores.justLoaded) {
|
||||||
this.semaphores.justLoaded = false;
|
this.semaphores.justLoaded = false;
|
||||||
if (!this.semaphores.preventAutozoom) {
|
if (!this.semaphores.preventAutozoom) {
|
||||||
this.zoomToFit(false);
|
this.zoomToFit(false,true);
|
||||||
}
|
}
|
||||||
this.previousSceneVersion = this.getSceneVersion(et);
|
this.previousSceneVersion = this.getSceneVersion(et);
|
||||||
this.previousBackgroundColor = st.viewBackgroundColor;
|
this.previousBackgroundColor = st.viewBackgroundColor;
|
||||||
@@ -3396,13 +3395,17 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public zoomToFit(delay: boolean = true) {
|
public zoomToFit(delay: boolean = true, justLoaded: boolean = false) {
|
||||||
const api = this.excalidrawAPI;
|
const api = this.excalidrawAPI;
|
||||||
if (!api || !this.excalidrawRef || this.semaphores.isEditingText) {
|
if (!api || !this.excalidrawRef || this.semaphores.isEditingText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
||||||
const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000);
|
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) {
|
if (delay) {
|
||||||
//time for the DOM to render, I am sure there is a more elegant solution
|
//time for the DOM to render, I am sure there is a more elegant solution
|
||||||
setTimeout(
|
setTimeout(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ export default {
|
|||||||
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
|
||||||
INSERT_LINK: "Insert link to file",
|
INSERT_LINK: "Insert link to file",
|
||||||
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
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_MD: "Insert markdown file from vault",
|
||||||
INSERT_LATEX:
|
INSERT_LATEX:
|
||||||
"Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
|
"Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!})",
|
||||||
|
|||||||
+42
-2
@@ -58,6 +58,7 @@ import {
|
|||||||
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
||||||
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
||||||
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
||||||
|
import { ImportSVGDialog } from "./dialogs/ImportSVGDialog";
|
||||||
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
||||||
import {
|
import {
|
||||||
initExcalidrawAutomate,
|
initExcalidrawAutomate,
|
||||||
@@ -139,6 +140,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
private openDialog: OpenFileDialog;
|
private openDialog: OpenFileDialog;
|
||||||
public insertLinkDialog: InsertLinkDialog;
|
public insertLinkDialog: InsertLinkDialog;
|
||||||
public insertImageDialog: InsertImageDialog;
|
public insertImageDialog: InsertImageDialog;
|
||||||
|
public importSVGDialog: ImportSVGDialog;
|
||||||
public insertMDDialog: InsertMDDialog;
|
public insertMDDialog: InsertMDDialog;
|
||||||
public activeExcalidrawView: ExcalidrawView = null;
|
public activeExcalidrawView: ExcalidrawView = null;
|
||||||
public lastActiveExcalidrawFilePath: string = null;
|
public lastActiveExcalidrawFilePath: string = null;
|
||||||
@@ -166,6 +168,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
||||||
public leafChangeTimeout: NodeJS.Timeout = null;
|
public leafChangeTimeout: NodeJS.Timeout = null;
|
||||||
private forceSaveCommand:Command;
|
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) {
|
constructor(app: App, manifest: PluginManifest) {
|
||||||
super(app, manifest);
|
super(app, manifest);
|
||||||
@@ -176,8 +189,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
this.equationsMaster = new Map<FileId, string>();
|
this.equationsMaster = new Map<FileId, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public getPackage(win:Window):Packages {
|
public getPackage(win:Window):Packages {
|
||||||
if(win===window) {
|
if(win===window) {
|
||||||
return {react, reactDOM, excalidrawLib};
|
return {react, reactDOM, excalidrawLib};
|
||||||
@@ -197,6 +208,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onload() {
|
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(ICON_NAME, EXCALIDRAW_ICON);
|
||||||
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
||||||
addIcon(DISK_ICON_NAME, DISK_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.openDialog = new OpenFileDialog(this.app, this);
|
||||||
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
||||||
this.insertImageDialog = new InsertImageDialog(this);
|
this.insertImageDialog = new InsertImageDialog(this);
|
||||||
|
this.importSVGDialog = new ImportSVGDialog(this);
|
||||||
this.insertMDDialog = new InsertMDDialog(this);
|
this.insertMDDialog = new InsertMDDialog(this);
|
||||||
|
|
||||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
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({
|
this.addCommand({
|
||||||
id: "release-notes",
|
id: "release-notes",
|
||||||
name: t("READ_RELEASE_NOTES"),
|
name: t("READ_RELEASE_NOTES"),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -473,6 +473,15 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
|||||||
icon={ICONS.copyElementLink}
|
icon={ICONS.copyElementLink}
|
||||||
view={this.props.view}
|
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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{this.renderScriptButtons(false)}
|
{this.renderScriptButtons(false)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as path from "./path";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
|
|||||||
@@ -1676,6 +1676,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/codemirror@0.0.108":
|
||||||
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
|
"integrity" "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw=="
|
||||||
"resolved" "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
"resolved" "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
||||||
@@ -2964,6 +2969,11 @@
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"fsevents" "~2.3.2"
|
"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":
|
"chrome-trace-event@^1.0.2":
|
||||||
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
"integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
||||||
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
|
"resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz"
|
||||||
@@ -4628,6 +4638,11 @@
|
|||||||
"call-bind" "^1.0.2"
|
"call-bind" "^1.0.2"
|
||||||
"get-intrinsic" "^1.1.1"
|
"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":
|
"glob-parent@^5.1.2", "glob-parent@~5.1.2":
|
||||||
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
|
"integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="
|
||||||
"resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
"resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user