diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json
index ce0cd9f..e1d9519 100644
--- a/.obsidian/community-plugins.json
+++ b/.obsidian/community-plugins.json
@@ -1,5 +1,8 @@
[
"obsidian-git",
"obsidian-tasks-plugin",
- "pdf-plus"
+ "pdf-plus",
+ "advanced-canvas",
+ "obsidian-plantuml",
+ "tasks-calendar-wrapper"
]
\ No newline at end of file
diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json
index 3f2ab43..639b90d 100644
--- a/.obsidian/core-plugins.json
+++ b/.obsidian/core-plugins.json
@@ -28,5 +28,6 @@
"file-recovery": true,
"publish": false,
"sync": true,
- "bases": true
+ "bases": true,
+ "webviewer": false
}
\ No newline at end of file
diff --git a/.obsidian/plugins/advanced-canvas/main.js b/.obsidian/plugins/advanced-canvas/main.js
new file mode 100644
index 0000000..0d90e25
--- /dev/null
+++ b/.obsidian/plugins/advanced-canvas/main.js
@@ -0,0 +1,7552 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+if you want to view the source, please visit the github repository of this plugin
+*/
+
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __export = (target, all) => {
+ for (var name in all)
+ __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/main.ts
+var main_exports = {};
+__export(main_exports, {
+ default: () => AdvancedCanvasPlugin
+});
+module.exports = __toCommonJS(main_exports);
+var import_obsidian19 = require("obsidian");
+
+// src/utils/icons-helper.ts
+var import_obsidian = require("obsidian");
+var CUSTOM_ICONS = {
+ "shape-pill": ``,
+ "shape-parallelogram": ``,
+ "shape-predefined-process": `
+
+
+
+
+
+ `,
+ "shape-document": ``,
+ "shape-database": `
+
+
+
+
+ `,
+ "border-solid": ``,
+ "border-dashed": ``,
+ "border-dotted": ``,
+ "path-solid": ``,
+ "path-dotted": ``,
+ "path-short-dashed": ``,
+ "path-long-dashed": ``,
+ "arrow-triangle": ``,
+ "arrow-triangle-outline": ``,
+ "arrow-thin-triangle": ``,
+ "arrow-halved-triangle": ``,
+ "arrow-diamond": ``,
+ "arrow-diamond-outline": ``,
+ "arrow-circle": ``,
+ "arrow-circle-outline": ``,
+ "pathfinding-method-bezier": ``,
+ "pathfinding-method-square": ``,
+ "arrows-selected": `
+
+
+
+
+
+
+
+
+
+ `,
+ "arrow-right-selected": `
+
+
+
+
+
+
+
+ `,
+ "arrow-left-selected": `
+
+
+
+
+
+
+
+ `
+};
+var IconsHelper = class {
+ static addIcons() {
+ for (const [id, svg] of Object.entries(CUSTOM_ICONS)) {
+ (0, import_obsidian.addIcon)(id, svg);
+ }
+ }
+};
+
+// src/utils/debug-helper.ts
+var DebugHelper = class {
+ constructor(plugin) {
+ this.logging = true;
+ this.nodeAddedCount = 0;
+ this.nodeChangedCount = 0;
+ this.edgeAddedCount = 0;
+ this.edgeChangedCount = 0;
+ this.plugin = plugin;
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (_canvas) => {
+ this.nodeAddedCount = 0;
+ this.nodeChangedCount = 0;
+ this.edgeAddedCount = 0;
+ this.edgeChangedCount = 0;
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-added",
+ (_canvas, _node) => {
+ if (this.logging) console.count("\u{1F7E2} NodeAdded");
+ this.nodeAddedCount++;
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-changed",
+ (_canvas, _node) => {
+ if (this.logging) console.count("\u{1F7E1} NodeChanged");
+ this.nodeChangedCount++;
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-added",
+ (_canvas, _edge) => {
+ if (this.logging) console.count("\u{1F7E2} EdgeAdded");
+ this.edgeAddedCount++;
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-changed",
+ (_canvas, _edge) => {
+ if (this.logging) console.count("\u{1F7E1} EdgeChanged");
+ this.edgeChangedCount++;
+ }
+ ));
+ }
+ resetEfficiency() {
+ this.nodeAddedCount = 0;
+ this.nodeChangedCount = 0;
+ this.edgeAddedCount = 0;
+ this.edgeChangedCount = 0;
+ }
+ logEfficiency() {
+ const canvas = this.plugin.getCurrentCanvas();
+ if (!canvas) return;
+ console.log("NodeAdded Efficiency:", this.nodeAddedCount / canvas.nodes.size);
+ console.log("NodeChanged Efficiency:", this.nodeChangedCount / canvas.nodes.size);
+ console.log("EdgeAdded Efficiency:", this.edgeAddedCount / canvas.edges.size);
+ console.log("EdgeChanged Efficiency:", this.edgeChangedCount / canvas.edges.size);
+ }
+ static markBBox(canvas, bbox, duration = -1) {
+ const node = canvas.createTextNode({
+ pos: { x: bbox.minX, y: bbox.minY },
+ size: { width: bbox.maxX - bbox.minX, height: bbox.maxY - bbox.minY },
+ text: "",
+ focus: false
+ });
+ node.setData({
+ ...node.getData(),
+ id: "debug-bbox",
+ color: "1",
+ styleAttributes: {
+ border: "invisible"
+ }
+ });
+ if (duration >= 0) {
+ setTimeout(() => {
+ canvas.removeNode(node);
+ }, duration);
+ }
+ }
+};
+
+// src/settings.ts
+var import_obsidian4 = require("obsidian");
+
+// src/utils/bbox-helper.ts
+var BBoxHelper = class {
+ static combineBBoxes(bboxes) {
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+ for (const bbox of bboxes) {
+ minX = Math.min(minX, bbox.minX);
+ minY = Math.min(minY, bbox.minY);
+ maxX = Math.max(maxX, bbox.maxX);
+ maxY = Math.max(maxY, bbox.maxY);
+ }
+ return { minX, minY, maxX, maxY };
+ }
+ static scaleBBox(bbox, scale) {
+ const diffX = (scale - 1) * (bbox.maxX - bbox.minX);
+ const diffY = (scale - 1) * (bbox.maxY - bbox.minY);
+ return {
+ minX: bbox.minX - diffX / 2,
+ maxX: bbox.maxX + diffX / 2,
+ minY: bbox.minY - diffY / 2,
+ maxY: bbox.maxY + diffY / 2
+ };
+ }
+ static isColliding(bbox1, bbox2) {
+ return bbox1.minX <= bbox2.maxX && bbox1.maxX >= bbox2.minX && bbox1.minY <= bbox2.maxY && bbox1.maxY >= bbox2.minY;
+ }
+ static insideBBox(position, bbox, canTouchEdge) {
+ if ("x" in position) {
+ const x = position.x, y = position.y;
+ return canTouchEdge ? x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY : x > bbox.minX && x < bbox.maxX && y > bbox.minY && y < bbox.maxY;
+ }
+ return canTouchEdge ? position.minX >= bbox.minX && position.maxX <= bbox.maxX && position.minY >= bbox.minY && position.maxY <= bbox.maxY : position.minX > bbox.minX && position.maxX < bbox.maxX && position.minY > bbox.minY && position.maxY < bbox.maxY;
+ }
+ static enlargeBBox(bbox, padding) {
+ return {
+ minX: bbox.minX - padding,
+ minY: bbox.minY - padding,
+ maxX: bbox.maxX + padding,
+ maxY: bbox.maxY + padding
+ };
+ }
+ static moveInDirection(position, side, distance) {
+ switch (side) {
+ case "top":
+ return { x: position.x, y: position.y - distance };
+ case "right":
+ return { x: position.x + distance, y: position.y };
+ case "bottom":
+ return { x: position.x, y: position.y + distance };
+ case "left":
+ return { x: position.x - distance, y: position.y };
+ }
+ }
+ static getCenterOfBBoxSide(bbox, side) {
+ switch (side) {
+ case "top":
+ return { x: (bbox.minX + bbox.maxX) / 2, y: bbox.minY };
+ case "right":
+ return { x: bbox.maxX, y: (bbox.minY + bbox.maxY) / 2 };
+ case "bottom":
+ return { x: (bbox.minX + bbox.maxX) / 2, y: bbox.maxY };
+ case "left":
+ return { x: bbox.minX, y: (bbox.minY + bbox.maxY) / 2 };
+ }
+ }
+ static getSideVector(side) {
+ switch (side) {
+ case "top":
+ return { x: 0, y: 1 };
+ case "right":
+ return { x: 1, y: 0 };
+ case "bottom":
+ return { x: 0, y: -1 };
+ case "left":
+ return { x: -1, y: 0 };
+ default:
+ return { x: 0, y: 0 };
+ }
+ }
+ static getOppositeSide(side) {
+ switch (side) {
+ case "top":
+ return "bottom";
+ case "right":
+ return "left";
+ case "bottom":
+ return "top";
+ case "left":
+ return "right";
+ }
+ }
+ static isHorizontal(side) {
+ return side === "left" || side === "right";
+ }
+ static direction(side) {
+ return side === "right" || side === "bottom" ? 1 : -1;
+ }
+};
+
+// src/utils/canvas-helper.ts
+var import_obsidian2 = require("obsidian");
+var _CanvasHelper = class _CanvasHelper {
+ static canvasCommand(plugin, check, run) {
+ return (checking) => {
+ const canvas = plugin.getCurrentCanvas();
+ if (checking) return canvas !== null && check(canvas);
+ if (canvas) run(canvas);
+ return true;
+ };
+ }
+ static createControlMenuButton(menuOption) {
+ const quickSetting = document.createElement("div");
+ if (menuOption.id) quickSetting.id = menuOption.id;
+ quickSetting.classList.add("canvas-control-item");
+ (0, import_obsidian2.setIcon)(quickSetting, menuOption.icon);
+ (0, import_obsidian2.setTooltip)(quickSetting, menuOption.label, { placement: "left" });
+ quickSetting.addEventListener("click", () => {
+ var _a;
+ return (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption);
+ });
+ return quickSetting;
+ }
+ static addControlMenuButton(controlGroup, element) {
+ var _a;
+ if (element.id) (_a = controlGroup.querySelector(`#${element.id}`)) == null ? void 0 : _a.remove();
+ controlGroup.appendChild(element);
+ }
+ static createCardMenuOption(canvas, menuOption, previewNodeSize, onPlaced) {
+ const menuOptionElement = document.createElement("div");
+ if (menuOption.id) menuOptionElement.id = menuOption.id;
+ menuOptionElement.classList.add("canvas-card-menu-button");
+ menuOptionElement.classList.add("mod-draggable");
+ (0, import_obsidian2.setIcon)(menuOptionElement, menuOption.icon);
+ (0, import_obsidian2.setTooltip)(menuOptionElement, menuOption.label, { placement: "top" });
+ menuOptionElement.addEventListener("click", (_e) => {
+ onPlaced(canvas, this.getCenterCoordinates(canvas, previewNodeSize()));
+ });
+ menuOptionElement.addEventListener("pointerdown", (e) => {
+ canvas.dragTempNode(e, previewNodeSize(), (pos) => {
+ canvas.deselectAll();
+ onPlaced(canvas, pos);
+ });
+ });
+ return menuOptionElement;
+ }
+ static addCardMenuOption(canvas, element) {
+ var _a;
+ if (element.id) (_a = canvas == null ? void 0 : canvas.cardMenuEl.querySelector(`#${element.id}`)) == null ? void 0 : _a.remove();
+ canvas == null ? void 0 : canvas.cardMenuEl.appendChild(element);
+ }
+ static createPopupMenuOption(menuOption) {
+ const menuOptionElement = document.createElement("button");
+ if (menuOption.id) menuOptionElement.id = menuOption.id;
+ menuOptionElement.classList.add("clickable-icon");
+ (0, import_obsidian2.setIcon)(menuOptionElement, menuOption.icon);
+ (0, import_obsidian2.setTooltip)(menuOptionElement, menuOption.label, { placement: "top" });
+ menuOptionElement.addEventListener("click", () => {
+ var _a;
+ return (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption);
+ });
+ return menuOptionElement;
+ }
+ static createExpandablePopupMenuOption(menuOption, subMenuOptions) {
+ const menuOptionElement = this.createPopupMenuOption({
+ ...menuOption,
+ callback: () => {
+ var _a, _b, _c;
+ const submenuId = `${menuOption.id}-submenu`;
+ if (menuOptionElement.classList.contains("is-active")) {
+ menuOptionElement.classList.remove("is-active");
+ (_b = (_a = menuOptionElement.parentElement) == null ? void 0 : _a.querySelector(`#${submenuId}`)) == null ? void 0 : _b.remove();
+ return;
+ }
+ menuOptionElement.classList.add("is-active");
+ const submenu = document.createElement("div");
+ submenu.id = submenuId;
+ submenu.classList.add("canvas-submenu");
+ for (const subMenuOption of subMenuOptions) {
+ const subMenuOptionElement = this.createPopupMenuOption(subMenuOption);
+ submenu.appendChild(subMenuOptionElement);
+ }
+ (_c = menuOptionElement.parentElement) == null ? void 0 : _c.appendChild(submenu);
+ }
+ });
+ return menuOptionElement;
+ }
+ static addPopupMenuOption(canvas, element, index = -1) {
+ var _a;
+ const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl;
+ if (!popupMenuEl) return;
+ if (element.id) {
+ const optionToReplace = popupMenuEl.querySelector(`#${element.id}`);
+ if (optionToReplace && index === -1) index = Array.from(popupMenuEl.children).indexOf(optionToReplace) - 1;
+ optionToReplace == null ? void 0 : optionToReplace.remove();
+ }
+ const sisterElement = index >= 0 ? popupMenuEl.children[index] : popupMenuEl.children[popupMenuEl.children.length + index];
+ popupMenuEl.insertAfter(element, sisterElement);
+ }
+ static getCenterCoordinates(canvas, nodeSize) {
+ const viewBounds = canvas.getViewportBBox();
+ return {
+ x: (viewBounds.minX + viewBounds.maxX) / 2 - nodeSize.width / 2,
+ y: (viewBounds.minY + viewBounds.maxY) / 2 - nodeSize.height / 2
+ };
+ }
+ static getBBox(canvasElements) {
+ const bBoxes = canvasElements.map((element) => {
+ if (element.getBBox) return element.getBBox();
+ const nodeData = element;
+ if (nodeData.x !== void 0 && nodeData.y !== void 0 && nodeData.width !== void 0 && nodeData.height !== void 0)
+ return { minX: nodeData.x, minY: nodeData.y, maxX: nodeData.x + nodeData.width, maxY: nodeData.y + nodeData.height };
+ return null;
+ }).filter((bbox) => bbox !== null);
+ return BBoxHelper.combineBBoxes(bBoxes);
+ }
+ static getSmallestAllowedZoomBBox(canvas, bbox) {
+ if (canvas.screenshotting) return bbox;
+ if (canvas.canvasRect.width === 0 || canvas.canvasRect.height === 0) return bbox;
+ const widthZoom = canvas.canvasRect.width / (bbox.maxX - bbox.minX);
+ const heightZoom = canvas.canvasRect.height / (bbox.maxY - bbox.minY);
+ const requiredZoom = Math.min(widthZoom, heightZoom);
+ if (requiredZoom > _CanvasHelper.MAX_ALLOWED_ZOOM) {
+ const scaleFactor = requiredZoom / _CanvasHelper.MAX_ALLOWED_ZOOM;
+ return BBoxHelper.scaleBBox(bbox, scaleFactor);
+ }
+ return bbox;
+ }
+ static addStyleAttributesToPopup(plugin, canvas, styleAttributes, currentStyleAttributes, setStyleAttribute) {
+ if (!plugin.settings.getSetting("combineCustomStylesInDropdown"))
+ this.addStyleAttributesButtons(canvas, styleAttributes, currentStyleAttributes, setStyleAttribute);
+ else this.addStyleAttributesDropdownMenu(canvas, styleAttributes, currentStyleAttributes, setStyleAttribute);
+ }
+ static addStyleAttributesButtons(canvas, stylableAttributes, currentStyleAttributes, setStyleAttribute) {
+ var _a;
+ for (const stylableAttribute of stylableAttributes) {
+ const selectedStyle = (_a = stylableAttribute.options.find((option) => currentStyleAttributes[stylableAttribute.key] === option.value)) != null ? _a : stylableAttribute.options.find((value) => value.value === null);
+ if (!selectedStyle) {
+ console.warn(`No "null" style option found for stylable attribute "${stylableAttribute.key}"`);
+ continue;
+ }
+ const menuOption = _CanvasHelper.createExpandablePopupMenuOption({
+ id: `menu-option-${stylableAttribute.key}`,
+ label: stylableAttribute.label,
+ icon: selectedStyle.icon
+ }, stylableAttribute.options.map((styleOption) => ({
+ label: styleOption.label,
+ icon: styleOption.icon,
+ callback: () => {
+ setStyleAttribute(stylableAttribute, styleOption.value);
+ currentStyleAttributes[stylableAttribute.key] = styleOption.value;
+ (0, import_obsidian2.setIcon)(menuOption, styleOption.icon);
+ menuOption.dispatchEvent(new Event("click"));
+ }
+ })));
+ _CanvasHelper.addPopupMenuOption(canvas, menuOption);
+ }
+ }
+ static addStyleAttributesDropdownMenu(canvas, stylableAttributes, currentStyleAttributes, setStyleAttribute) {
+ var _a, _b;
+ const STYLE_MENU_ID = "style-menu";
+ const STYLE_MENU_DROPDOWN_ID = "style-menu-dropdown";
+ const STYLE_MENU_DROPDOWN_SUBMENU_ID = "style-menu-dropdown-submenu";
+ const popupMenuElement = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl;
+ if (!popupMenuElement) return;
+ (_b = popupMenuElement.querySelector(`#${STYLE_MENU_ID}`)) == null ? void 0 : _b.remove();
+ const styleMenuButtonElement = document.createElement("button");
+ styleMenuButtonElement.id = STYLE_MENU_ID;
+ styleMenuButtonElement.classList.add("clickable-icon");
+ (0, import_obsidian2.setIcon)(styleMenuButtonElement, "paintbrush");
+ (0, import_obsidian2.setTooltip)(styleMenuButtonElement, "Style", { placement: "top" });
+ popupMenuElement.appendChild(styleMenuButtonElement);
+ styleMenuButtonElement.addEventListener("click", () => {
+ var _a2, _b2, _c;
+ const isOpen = styleMenuButtonElement.classList.toggle("has-active-menu");
+ if (!isOpen) {
+ (_a2 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_ID}`)) == null ? void 0 : _a2.remove();
+ (_b2 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_SUBMENU_ID}`)) == null ? void 0 : _b2.remove();
+ return;
+ }
+ const styleMenuDropdownElement = document.createElement("div");
+ styleMenuDropdownElement.id = STYLE_MENU_DROPDOWN_ID;
+ styleMenuDropdownElement.classList.add("menu");
+ styleMenuDropdownElement.style.position = "absolute";
+ styleMenuDropdownElement.style.maxHeight = "initial";
+ styleMenuDropdownElement.style.top = `${popupMenuElement.getBoundingClientRect().height}px`;
+ const canvasWrapperCenterX = canvas.wrapperEl.getBoundingClientRect().left + canvas.wrapperEl.getBoundingClientRect().width / 2;
+ const leftPosition = styleMenuButtonElement.getBoundingClientRect().left - popupMenuElement.getBoundingClientRect().left;
+ const rightPosition = popupMenuElement.getBoundingClientRect().right - styleMenuButtonElement.getBoundingClientRect().right;
+ if (popupMenuElement.getBoundingClientRect().left + leftPosition < canvasWrapperCenterX)
+ styleMenuDropdownElement.style.left = `${leftPosition}px`;
+ else styleMenuDropdownElement.style.right = `${rightPosition}px`;
+ for (const stylableAttribute of stylableAttributes) {
+ const stylableAttributeElement = document.createElement("div");
+ stylableAttributeElement.classList.add("menu-item");
+ stylableAttributeElement.classList.add("tappable");
+ const iconElement = document.createElement("div");
+ iconElement.classList.add("menu-item-icon");
+ let selectedStyle = (_c = stylableAttribute.options.find((option) => currentStyleAttributes[stylableAttribute.key] === option.value)) != null ? _c : stylableAttribute.options.find((value) => value.value === null);
+ if (!selectedStyle) continue;
+ (0, import_obsidian2.setIcon)(iconElement, selectedStyle.icon);
+ stylableAttributeElement.appendChild(iconElement);
+ const labelElement = document.createElement("div");
+ labelElement.classList.add("menu-item-title");
+ labelElement.textContent = stylableAttribute.label;
+ stylableAttributeElement.appendChild(labelElement);
+ const expandIconElement = document.createElement("div");
+ expandIconElement.classList.add("menu-item-icon");
+ (0, import_obsidian2.setIcon)(expandIconElement, "chevron-right");
+ stylableAttributeElement.appendChild(expandIconElement);
+ styleMenuDropdownElement.appendChild(stylableAttributeElement);
+ stylableAttributeElement.addEventListener("pointerenter", () => {
+ stylableAttributeElement.classList.add("selected");
+ });
+ stylableAttributeElement.addEventListener("pointerleave", () => {
+ stylableAttributeElement.classList.remove("selected");
+ });
+ stylableAttributeElement.addEventListener("click", () => {
+ var _a3;
+ (_a3 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_SUBMENU_ID}`)) == null ? void 0 : _a3.remove();
+ const styleMenuDropdownSubmenuElement = document.createElement("div");
+ styleMenuDropdownSubmenuElement.id = STYLE_MENU_DROPDOWN_SUBMENU_ID;
+ styleMenuDropdownSubmenuElement.classList.add("menu");
+ styleMenuDropdownSubmenuElement.style.position = "absolute";
+ styleMenuDropdownSubmenuElement.style.maxHeight = "initial";
+ const topOffset = parseFloat(window.getComputedStyle(styleMenuDropdownElement).getPropertyValue("padding-top")) + (styleMenuDropdownElement.offsetHeight - styleMenuDropdownElement.clientHeight) / 2;
+ styleMenuDropdownSubmenuElement.style.top = `${stylableAttributeElement.getBoundingClientRect().top - topOffset - popupMenuElement.getBoundingClientRect().top}px`;
+ const leftPosition2 = styleMenuDropdownElement.getBoundingClientRect().right - popupMenuElement.getBoundingClientRect().left;
+ const rightPosition2 = popupMenuElement.getBoundingClientRect().right - styleMenuDropdownElement.getBoundingClientRect().left;
+ if (popupMenuElement.getBoundingClientRect().left + leftPosition2 < canvasWrapperCenterX)
+ styleMenuDropdownSubmenuElement.style.left = `${leftPosition2}px`;
+ else styleMenuDropdownSubmenuElement.style.right = `${rightPosition2}px`;
+ for (const styleOption of stylableAttribute.options) {
+ const styleMenuDropdownSubmenuOptionElement = this.createDropdownOptionElement({
+ label: styleOption.label,
+ icon: styleOption.icon,
+ callback: () => {
+ setStyleAttribute(stylableAttribute, styleOption.value);
+ currentStyleAttributes[stylableAttribute.key] = styleOption.value;
+ selectedStyle = styleOption;
+ (0, import_obsidian2.setIcon)(iconElement, styleOption.icon);
+ styleMenuDropdownSubmenuElement.remove();
+ }
+ });
+ if (selectedStyle === styleOption) {
+ styleMenuDropdownSubmenuOptionElement.classList.add("mod-selected");
+ const selectedIconElement = document.createElement("div");
+ selectedIconElement.classList.add("menu-item-icon");
+ selectedIconElement.classList.add("mod-selected");
+ (0, import_obsidian2.setIcon)(selectedIconElement, "check");
+ styleMenuDropdownSubmenuOptionElement.appendChild(selectedIconElement);
+ }
+ styleMenuDropdownSubmenuElement.appendChild(styleMenuDropdownSubmenuOptionElement);
+ }
+ popupMenuElement.appendChild(styleMenuDropdownSubmenuElement);
+ });
+ }
+ popupMenuElement.appendChild(styleMenuDropdownElement);
+ });
+ }
+ static createDropdownOptionElement(menuOption) {
+ const menuDropdownOptionElement = document.createElement("div");
+ menuDropdownOptionElement.classList.add("menu-item");
+ menuDropdownOptionElement.classList.add("tappable");
+ const iconElement = document.createElement("div");
+ iconElement.classList.add("menu-item-icon");
+ (0, import_obsidian2.setIcon)(iconElement, menuOption.icon);
+ menuDropdownOptionElement.appendChild(iconElement);
+ const labelElement = document.createElement("div");
+ labelElement.classList.add("menu-item-title");
+ labelElement.textContent = menuOption.label;
+ menuDropdownOptionElement.appendChild(labelElement);
+ menuDropdownOptionElement.addEventListener("pointerenter", () => {
+ menuDropdownOptionElement.classList.add("selected");
+ });
+ menuDropdownOptionElement.addEventListener("pointerleave", () => {
+ menuDropdownOptionElement.classList.remove("selected");
+ });
+ menuDropdownOptionElement.addEventListener("click", () => {
+ var _a;
+ (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption);
+ });
+ return menuDropdownOptionElement;
+ }
+ static createDropdownSeparatorElement() {
+ const separatorElement = document.createElement("div");
+ separatorElement.classList.add("menu-separator");
+ return separatorElement;
+ }
+ static alignToGrid(value, gridSize = this.GRID_SIZE) {
+ return Math.round(value / gridSize) * gridSize;
+ }
+ static getBestSideForFloatingEdge(sourcePos, target) {
+ const targetBBox = target.getBBox();
+ const possibleSides = ["top", "right", "bottom", "left"];
+ const possibleTargetPos = possibleSides.map((side) => [side, BBoxHelper.getCenterOfBBoxSide(targetBBox, side)]);
+ let bestSide = null;
+ let bestDistance = Infinity;
+ for (const [side, pos] of possibleTargetPos) {
+ const distance = Math.sqrt(Math.pow(sourcePos.x - pos.x, 2) + Math.pow(sourcePos.y - pos.y, 2));
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestSide = side;
+ }
+ }
+ return bestSide;
+ }
+ static selectEdgesForNodes(canvas, direction) {
+ const selection = canvas.getSelectionData();
+ if (selection.nodes.length === 0) return;
+ const edges = /* @__PURE__ */ new Set();
+ for (const nodeData of selection.nodes) {
+ const node = canvas.nodes.get(nodeData.id);
+ if (!node) continue;
+ for (const edge of canvas.getEdgesForNode(node)) {
+ switch (direction) {
+ case "connected":
+ edges.add(edge);
+ break;
+ case "incoming":
+ if (edge.to.node === node) edges.add(edge);
+ break;
+ case "outgoing":
+ if (edge.from.node === node) edges.add(edge);
+ break;
+ }
+ }
+ }
+ canvas.updateSelection(() => {
+ canvas.selection = edges;
+ });
+ }
+};
+_CanvasHelper.GRID_SIZE = 20;
+_CanvasHelper.MAX_ALLOWED_ZOOM = 1;
+var CanvasHelper = _CanvasHelper;
+
+// src/canvas-extensions/canvas-extension.ts
+var CanvasExtension = class {
+ constructor(plugin) {
+ this.plugin = plugin;
+ const isEnabled = this.isEnabled();
+ if (!(isEnabled === true || this.plugin.settings.getSetting(isEnabled))) return;
+ this.init();
+ }
+};
+
+// src/utils/svg-path-helper.ts
+var SvgPathHelper = class {
+ static smoothenPathArray(positions, tension) {
+ let newPositions = [...positions];
+ if (positions.length <= 2) return newPositions;
+ newPositions = [positions[0]];
+ for (let i = 1; i < positions.length - 2; i++) {
+ const p1 = positions[i];
+ const p2 = positions[i + 1];
+ const p3 = positions[i + 2];
+ const t1 = (1 - tension) / 2;
+ const t2 = 1 - t1;
+ const x = t2 * t2 * t2 * p1.x + 3 * t2 * t2 * t1 * p2.x + 3 * t2 * t1 * t1 * p3.x + t1 * t1 * t1 * p2.x;
+ const y = t2 * t2 * t2 * p1.y + 3 * t2 * t2 * t1 * p2.y + 3 * t2 * t1 * t1 * p3.y + t1 * t1 * t1 * p2.y;
+ newPositions.push({ x, y });
+ }
+ const lastPoint = positions[positions.length - 1];
+ newPositions.push(lastPoint);
+ return newPositions;
+ }
+ static pathArrayToSvgPath(positions) {
+ for (let i = 0; i < positions.length - 2; i++) {
+ const p1 = positions[i];
+ const p2 = positions[i + 1];
+ const p3 = positions[i + 2];
+ const currentDirection = {
+ x: p2.x - p1.x,
+ y: p2.y - p1.y
+ };
+ const nextDirection = {
+ x: p3.x - p2.x,
+ y: p3.y - p2.y
+ };
+ if (currentDirection.x !== nextDirection.x && currentDirection.y !== nextDirection.y) continue;
+ positions.splice(i + 1, 1);
+ i--;
+ }
+ return positions.map(
+ (position, index) => `${index === 0 ? "M" : "L"} ${position.x} ${position.y}`
+ ).join(" ");
+ }
+ static pathArrayToRoundedSvgPath(pathArray, targetRadius) {
+ if (pathArray.length < 3)
+ return this.pathArrayToSvgPath(pathArray);
+ pathArray = pathArray.filter((position, index) => {
+ if (index === 0) return true;
+ const previous = pathArray[index - 1];
+ return !(position.x === previous.x && position.y === previous.y);
+ });
+ const commands = [];
+ commands.push(`M ${pathArray[0].x} ${pathArray[0].y}`);
+ for (let i = 1; i < pathArray.length - 1; i++) {
+ const previous = pathArray[i - 1];
+ const current = pathArray[i];
+ const next = pathArray[i + 1];
+ const prevDelta = { x: current.x - previous.x, y: current.y - previous.y };
+ const nextDelta = { x: next.x - current.x, y: next.y - current.y };
+ const prevLen = Math.sqrt(prevDelta.x * prevDelta.x + prevDelta.y * prevDelta.y);
+ const nextLen = Math.sqrt(nextDelta.x * nextDelta.x + nextDelta.y * nextDelta.y);
+ const prevUnit = prevLen ? { x: prevDelta.x / prevLen, y: prevDelta.y / prevLen } : { x: 0, y: 0 };
+ const nextUnit = nextLen ? { x: nextDelta.x / nextLen, y: nextDelta.y / nextLen } : { x: 0, y: 0 };
+ let dot = prevUnit.x * nextUnit.x + prevUnit.y * nextUnit.y;
+ dot = Math.max(-1, Math.min(1, dot));
+ const angle = Math.acos(dot);
+ if (angle < 0.01 || Math.abs(Math.PI - angle) < 0.01) {
+ commands.push(`L ${current.x} ${current.y}`);
+ continue;
+ }
+ const desiredOffset = targetRadius * Math.tan(angle / 2);
+ const d = Math.min(desiredOffset, prevLen / 2, nextLen / 2);
+ const effectiveRadius = d / Math.tan(angle / 2);
+ const firstAnchor = {
+ x: current.x - prevUnit.x * d,
+ y: current.y - prevUnit.y * d
+ };
+ const secondAnchor = {
+ x: current.x + nextUnit.x * d,
+ y: current.y + nextUnit.y * d
+ };
+ commands.push(`L ${firstAnchor.x} ${firstAnchor.y}`);
+ const cross = prevDelta.x * nextDelta.y - prevDelta.y * nextDelta.x;
+ const sweepFlag = cross < 0 ? 0 : 1;
+ commands.push(`A ${effectiveRadius} ${effectiveRadius} 0 0 ${sweepFlag} ${secondAnchor.x} ${secondAnchor.y}`);
+ }
+ const last = pathArray[pathArray.length - 1];
+ commands.push(`L ${last.x} ${last.y}`);
+ return commands.join(" ");
+ }
+};
+
+// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/edge-pathfinding-method.ts
+var EdgePathfindingMethod = class {
+ constructor(plugin, canvas, fromNodeBBox, fromPos, fromBBoxSidePos, fromSide, toNodeBBox, toPos, toBBoxSidePos, toSide) {
+ this.plugin = plugin;
+ this.canvas = canvas;
+ this.fromNodeBBox = fromNodeBBox;
+ this.fromPos = fromPos;
+ this.fromBBoxSidePos = fromBBoxSidePos;
+ this.fromSide = fromSide;
+ this.toNodeBBox = toNodeBBox;
+ this.toPos = toPos;
+ this.toBBoxSidePos = toBBoxSidePos;
+ this.toSide = toSide;
+ }
+};
+
+// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-a-star.ts
+var MAX_MS_CALCULATION = 100;
+var BASIC_DIRECTIONS = [
+ { dx: 1, dy: 0 },
+ { dx: -1, dy: 0 },
+ { dx: 0, dy: 1 },
+ { dx: 0, dy: -1 }
+];
+var DIAGONAL_DIRECTIONS = [
+ { dx: 1, dy: 1 },
+ { dx: -1, dy: 1 },
+ { dx: 1, dy: -1 },
+ { dx: -1, dy: -1 }
+];
+var DIAGONAL_COST = Math.sqrt(2);
+var ROUND_PATH_RADIUS = 5;
+var SMOOTHEN_PATH_TENSION = 0.2;
+var Node = class {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.gCost = 0;
+ this.hCost = 0;
+ this.fCost = 0;
+ this.parent = null;
+ }
+ // Only check for x and y, not gCost, hCost, fCost, or parent
+ inList(nodes) {
+ return nodes.some((n) => n.x === this.x && n.y === this.y);
+ }
+};
+var EdgePathfindingAStar = class extends EdgePathfindingMethod {
+ getPath() {
+ const nodeBBoxes = [...this.canvas.nodes.values()].filter((node) => {
+ const nodeData = node.getData();
+ if (nodeData.portal === true) return false;
+ const nodeBBox = node.getBBox();
+ const nodeContainsFromPos = BBoxHelper.insideBBox(this.fromPos, nodeBBox, true);
+ const nodeContainsToPos = BBoxHelper.insideBBox(this.toPos, nodeBBox, true);
+ return !nodeContainsFromPos && !nodeContainsToPos;
+ }).map((node) => node.getBBox());
+ const fromPosWithMargin = BBoxHelper.moveInDirection(this.fromPos, this.fromSide, 10);
+ const toPosWithMargin = BBoxHelper.moveInDirection(this.toPos, this.toSide, 10);
+ const allowDiagonal = this.plugin.settings.getSetting("edgeStylePathfinderAllowDiagonal");
+ const pathArray = this.aStarAlgorithm(fromPosWithMargin, toPosWithMargin, nodeBBoxes, CanvasHelper.GRID_SIZE / 2, allowDiagonal);
+ if (!pathArray) return null;
+ pathArray.splice(0, 0, this.fromPos);
+ pathArray.splice(pathArray.length, 0, this.toPos);
+ let svgPath;
+ const roundPath = this.plugin.settings.getSetting("edgeStylePathfinderPathRounded");
+ if (roundPath) {
+ if (allowDiagonal)
+ svgPath = SvgPathHelper.pathArrayToSvgPath(SvgPathHelper.smoothenPathArray(pathArray, SMOOTHEN_PATH_TENSION));
+ else
+ svgPath = SvgPathHelper.pathArrayToRoundedSvgPath(pathArray, ROUND_PATH_RADIUS);
+ } else svgPath = SvgPathHelper.pathArrayToSvgPath(pathArray);
+ return {
+ svgPath,
+ center: pathArray[Math.floor(pathArray.length / 2)],
+ rotateArrows: false
+ };
+ }
+ aStarAlgorithm(fromPos, toPos, obstacles, gridResolution, allowDiagonal) {
+ const start = new Node(
+ Math.floor(fromPos.x / gridResolution) * gridResolution,
+ Math.floor(fromPos.y / gridResolution) * gridResolution
+ );
+ if (this.fromSide === "right" && fromPos.x !== start.x) start.x += gridResolution;
+ if (this.fromSide === "bottom" && fromPos.y !== start.y) start.y += gridResolution;
+ const end = new Node(
+ Math.floor(toPos.x / gridResolution) * gridResolution,
+ Math.floor(toPos.y / gridResolution) * gridResolution
+ );
+ if (this.toSide === "right" && toPos.x !== end.x) end.x += gridResolution;
+ if (this.toSide === "bottom" && toPos.y !== end.y) end.y += gridResolution;
+ if (this.isInsideObstacle(start, obstacles) || this.isInsideObstacle(end, obstacles)) return null;
+ const openSet = [start];
+ const closedSet = [];
+ const startTimestamp = performance.now();
+ while (openSet.length > 0) {
+ let current = null;
+ let lowestFCost = Infinity;
+ for (const node of openSet) {
+ if (node.fCost < lowestFCost) {
+ current = node;
+ lowestFCost = node.fCost;
+ }
+ }
+ if (performance.now() - startTimestamp > MAX_MS_CALCULATION)
+ return null;
+ if (!current)
+ return null;
+ openSet.splice(openSet.indexOf(current), 1);
+ closedSet.push(current);
+ if (current.x === end.x && current.y === end.y)
+ return [fromPos, ...this.reconstructPath(current), toPos].map((node) => ({ x: node.x, y: node.y }));
+ if (!(current.x === start.x && current.y === start.y) && this.isTouchingObstacle(current, obstacles))
+ continue;
+ for (const neighbor of this.getPossibleNeighbors(current, obstacles, gridResolution, allowDiagonal)) {
+ if (neighbor.inList(closedSet))
+ continue;
+ const tentativeGCost = current.gCost + (allowDiagonal ? this.getMovementCost({
+ dx: neighbor.x - current.x,
+ dy: neighbor.y - current.y
+ }) : 1);
+ if (!neighbor.inList(openSet) || tentativeGCost < neighbor.gCost) {
+ neighbor.parent = current;
+ neighbor.gCost = tentativeGCost;
+ neighbor.hCost = this.heuristic(neighbor, end);
+ neighbor.fCost = neighbor.gCost + neighbor.hCost;
+ openSet.push(neighbor);
+ }
+ }
+ }
+ return null;
+ }
+ // Manhattan distance
+ heuristic(node, end) {
+ return Math.abs(node.x - end.x) + Math.abs(node.y - end.y);
+ }
+ // Define a function to check if a position isn't inside any obstacle
+ isTouchingObstacle(node, obstacles) {
+ return obstacles.some((obstacle) => BBoxHelper.insideBBox(node, obstacle, true));
+ }
+ isInsideObstacle(node, obstacles) {
+ return obstacles.some((obstacle) => BBoxHelper.insideBBox(node, obstacle, false));
+ }
+ // Define a function to calculate movement cost based on direction
+ getMovementCost(direction) {
+ return direction.dx !== 0 && direction.dy !== 0 ? DIAGONAL_COST : 1;
+ }
+ getPossibleNeighbors(node, obstacles, gridResolution, allowDiagonal) {
+ const neighbors = [];
+ const availableDirections = allowDiagonal ? [...BASIC_DIRECTIONS, ...DIAGONAL_DIRECTIONS] : BASIC_DIRECTIONS;
+ for (const direction of availableDirections) {
+ const neighbor = new Node(
+ node.x + direction.dx * gridResolution,
+ node.y + direction.dy * gridResolution
+ );
+ neighbor.gCost = node.gCost + this.getMovementCost(direction);
+ if (this.isInsideObstacle(neighbor, obstacles)) continue;
+ neighbors.push(neighbor);
+ }
+ return neighbors;
+ }
+ reconstructPath(node) {
+ const path = [];
+ while (node) {
+ path.push(node);
+ node = node.parent;
+ }
+ return path.reverse();
+ }
+};
+
+// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-direct.ts
+var EdgePathfindingDirect = class extends EdgePathfindingMethod {
+ getPath() {
+ return {
+ svgPath: SvgPathHelper.pathArrayToSvgPath([this.fromPos, this.toPos]),
+ center: {
+ x: (this.fromPos.x + this.toPos.x) / 2,
+ y: (this.fromPos.y + this.toPos.y) / 2
+ },
+ rotateArrows: true
+ };
+ }
+};
+
+// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-square.ts
+var ROUNDED_EDGE_RADIUS = 5;
+var EdgePathfindingSquare = class extends EdgePathfindingMethod {
+ getPath() {
+ const pathArray = [];
+ let center = {
+ x: (this.fromPos.x + this.toPos.x) / 2,
+ y: (this.fromPos.y + this.toPos.y) / 2
+ };
+ const idealCenter = BBoxHelper.isHorizontal(this.fromSide) ? {
+ x: this.toBBoxSidePos.x,
+ y: this.fromBBoxSidePos.y
+ } : {
+ x: this.fromBBoxSidePos.x,
+ y: this.toBBoxSidePos.y
+ };
+ const isPathCollidingAtFrom = this.fromSide === "top" && idealCenter.y > this.fromPos.y || this.fromSide === "bottom" && idealCenter.y < this.fromPos.y || this.fromSide === "left" && idealCenter.x > this.fromPos.x || this.fromSide === "right" && idealCenter.x < this.fromPos.x;
+ const isPathCollidingAtTo = this.toSide === "top" && idealCenter.y > this.toPos.y || this.toSide === "bottom" && idealCenter.y < this.toPos.y || this.toSide === "left" && idealCenter.x > this.toPos.x || this.toSide === "right" && idealCenter.x < this.toPos.x;
+ if (this.fromSide === this.toSide) {
+ const uPath = this.getUPath(this.fromPos, this.toPos, this.fromSide, this.toSide);
+ pathArray.push(...uPath.pathArray);
+ center = uPath.center;
+ } else if (BBoxHelper.isHorizontal(this.fromSide) === BBoxHelper.isHorizontal(this.toSide)) {
+ let zPath;
+ if (!isPathCollidingAtFrom || !isPathCollidingAtTo) {
+ zPath = this.getZPath(this.fromPos, this.toPos, this.fromSide, this.toSide);
+ pathArray.push(...zPath.pathArray);
+ } else {
+ const fromDirection = BBoxHelper.direction(this.fromSide);
+ const firstFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? {
+ x: CanvasHelper.alignToGrid(this.fromBBoxSidePos.x + fromDirection * CanvasHelper.GRID_SIZE),
+ y: this.fromBBoxSidePos.y
+ } : {
+ x: this.fromBBoxSidePos.x,
+ y: CanvasHelper.alignToGrid(this.fromBBoxSidePos.y + fromDirection * CanvasHelper.GRID_SIZE)
+ };
+ const toDirection = BBoxHelper.direction(this.toSide);
+ const firstToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? {
+ x: CanvasHelper.alignToGrid(this.toBBoxSidePos.x + toDirection * CanvasHelper.GRID_SIZE),
+ y: this.toBBoxSidePos.y
+ } : {
+ x: this.toBBoxSidePos.x,
+ y: CanvasHelper.alignToGrid(this.toBBoxSidePos.y + toDirection * CanvasHelper.GRID_SIZE)
+ };
+ const newFromSide = BBoxHelper.isHorizontal(this.fromSide) ? firstFromDetourPoint.y < this.fromPos.y ? "top" : "bottom" : firstFromDetourPoint.x < firstToDetourPoint.x ? "right" : "left";
+ zPath = this.getZPath(firstFromDetourPoint, firstToDetourPoint, newFromSide, BBoxHelper.getOppositeSide(newFromSide));
+ pathArray.push(this.fromPos);
+ pathArray.push(...zPath.pathArray);
+ pathArray.push(this.toPos);
+ }
+ center = zPath.center;
+ } else {
+ if (isPathCollidingAtFrom || isPathCollidingAtTo) {
+ if (isPathCollidingAtFrom && isPathCollidingAtTo) {
+ const direction = BBoxHelper.direction(this.fromSide);
+ let firstFromDetourPoint;
+ let secondFromDetourPoint;
+ if (BBoxHelper.isHorizontal(this.fromSide)) {
+ const combinedBBoxes = BBoxHelper.combineBBoxes([this.fromNodeBBox, this.toNodeBBox]);
+ firstFromDetourPoint = {
+ x: CanvasHelper.alignToGrid((direction > 0 ? combinedBBoxes.maxX : combinedBBoxes.minX) + direction * CanvasHelper.GRID_SIZE),
+ y: this.fromBBoxSidePos.y
+ };
+ secondFromDetourPoint = {
+ x: firstFromDetourPoint.x,
+ y: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, this.toSide).y
+ };
+ } else {
+ const combinedBBoxes = BBoxHelper.combineBBoxes([this.fromNodeBBox, this.toNodeBBox]);
+ firstFromDetourPoint = {
+ x: this.fromBBoxSidePos.x,
+ y: CanvasHelper.alignToGrid((direction > 0 ? combinedBBoxes.maxY : combinedBBoxes.minY) + direction * CanvasHelper.GRID_SIZE)
+ };
+ secondFromDetourPoint = {
+ x: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, this.toSide).x,
+ y: firstFromDetourPoint.y
+ };
+ }
+ const uPath = this.getUPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide);
+ pathArray.push(this.fromPos);
+ pathArray.push(firstFromDetourPoint);
+ pathArray.push(...uPath.pathArray);
+ center = pathArray[Math.floor(pathArray.length / 2)];
+ } else {
+ if (isPathCollidingAtFrom) {
+ const direction = BBoxHelper.direction(this.fromSide);
+ const firstFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? {
+ x: CanvasHelper.alignToGrid(this.fromBBoxSidePos.x + direction * CanvasHelper.GRID_SIZE),
+ y: this.fromBBoxSidePos.y
+ } : {
+ x: this.fromBBoxSidePos.x,
+ y: CanvasHelper.alignToGrid(this.fromBBoxSidePos.y + direction * CanvasHelper.GRID_SIZE)
+ };
+ const useUPath = BBoxHelper.isHorizontal(this.fromSide) ? this.toPos.y > BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, BBoxHelper.getOppositeSide(this.toSide)).y === BBoxHelper.direction(this.toSide) > 0 : this.toPos.x > BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, BBoxHelper.getOppositeSide(this.toSide)).x === BBoxHelper.direction(this.toSide) > 0;
+ const connectionSide = useUPath ? this.toSide : BBoxHelper.getOppositeSide(this.toSide);
+ const secondFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? {
+ x: firstFromDetourPoint.x,
+ y: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, connectionSide).y
+ } : {
+ x: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, connectionSide).x,
+ y: firstFromDetourPoint.y
+ };
+ const path = useUPath ? this.getUPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide) : this.getZPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide);
+ pathArray.push(this.fromPos);
+ pathArray.push(firstFromDetourPoint);
+ pathArray.push(...path.pathArray);
+ center = path.center;
+ }
+ if (isPathCollidingAtTo) {
+ const direction = BBoxHelper.direction(this.toSide);
+ const firstToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? {
+ x: CanvasHelper.alignToGrid(this.toBBoxSidePos.x + direction * CanvasHelper.GRID_SIZE),
+ y: this.toBBoxSidePos.y
+ } : {
+ x: this.toBBoxSidePos.x,
+ y: CanvasHelper.alignToGrid(this.toBBoxSidePos.y + direction * CanvasHelper.GRID_SIZE)
+ };
+ const useUPath = BBoxHelper.isHorizontal(this.toSide) ? this.fromPos.y > BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, BBoxHelper.getOppositeSide(this.fromSide)).y === BBoxHelper.direction(this.fromSide) > 0 : this.fromPos.x > BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, BBoxHelper.getOppositeSide(this.fromSide)).x === BBoxHelper.direction(this.fromSide) > 0;
+ const connectionSide = useUPath ? this.fromSide : BBoxHelper.getOppositeSide(this.fromSide);
+ const secondToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? {
+ x: firstToDetourPoint.x,
+ y: BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, connectionSide).y
+ } : {
+ x: BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, connectionSide).x,
+ y: firstToDetourPoint.y
+ };
+ const path = useUPath ? this.getUPath(this.fromPos, secondToDetourPoint, this.fromSide, this.fromSide) : this.getZPath(this.fromPos, secondToDetourPoint, this.fromSide, this.fromSide);
+ pathArray.push(...path.pathArray);
+ pathArray.push(secondToDetourPoint);
+ pathArray.push(firstToDetourPoint);
+ pathArray.push(this.toPos);
+ center = path.center;
+ }
+ }
+ } else {
+ pathArray.push(
+ this.fromPos,
+ idealCenter,
+ this.toPos
+ );
+ center = {
+ x: pathArray[1].x,
+ y: pathArray[1].y
+ };
+ }
+ }
+ const svgPath = this.plugin.settings.getSetting("edgeStyleSquarePathRounded") ? SvgPathHelper.pathArrayToRoundedSvgPath(pathArray, ROUNDED_EDGE_RADIUS) : SvgPathHelper.pathArrayToSvgPath(pathArray);
+ return { svgPath, center, rotateArrows: false };
+ }
+ getUPath(fromPos, toPos, fromSide, toSide) {
+ const direction = BBoxHelper.direction(fromSide);
+ if (BBoxHelper.isHorizontal(fromSide)) {
+ const xExtremum = direction > 0 ? Math.max(fromPos.x, toPos.x) : Math.min(fromPos.x, toPos.x);
+ const x = CanvasHelper.alignToGrid(xExtremum + direction * CanvasHelper.GRID_SIZE);
+ return {
+ pathArray: [
+ fromPos,
+ { x, y: fromPos.y },
+ { x, y: toPos.y },
+ toPos
+ ],
+ center: {
+ x,
+ y: (fromPos.y + toPos.y) / 2
+ }
+ };
+ } else {
+ const yExtremum = direction > 0 ? Math.max(fromPos.y, toPos.y) : Math.min(fromPos.y, toPos.y);
+ const y = CanvasHelper.alignToGrid(yExtremum + direction * CanvasHelper.GRID_SIZE);
+ return {
+ pathArray: [
+ fromPos,
+ { x: fromPos.x, y },
+ { x: toPos.x, y },
+ toPos
+ ],
+ center: {
+ x: (fromPos.x + toPos.x) / 2,
+ y
+ }
+ };
+ }
+ }
+ getZPath(fromPos, toPos, fromSide, toSide) {
+ if (BBoxHelper.isHorizontal(fromSide)) {
+ const midX = fromPos.x + (toPos.x - fromPos.x) / 2;
+ return {
+ pathArray: [
+ fromPos,
+ { x: midX, y: fromPos.y },
+ { x: midX, y: toPos.y },
+ toPos
+ ],
+ center: {
+ x: midX,
+ y: (fromPos.y + toPos.y) / 2
+ }
+ };
+ } else {
+ const midY = fromPos.y + (toPos.y - fromPos.y) / 2;
+ return {
+ pathArray: [
+ fromPos,
+ { x: fromPos.x, y: midY },
+ { x: toPos.x, y: midY },
+ toPos
+ ],
+ center: {
+ x: (fromPos.x + toPos.x) / 2,
+ y: midY
+ }
+ };
+ }
+ }
+};
+
+// src/utils/text-helper.ts
+var TextHelper = class {
+ static toCamelCase(str) {
+ return str.replace(/-./g, (x) => x[1].toUpperCase());
+ }
+ static toTitleCase(str) {
+ return str.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
+ }
+};
+
+// src/canvas-extensions/advanced-styles/style-config.ts
+function styleAttributeValidator(json) {
+ var _a;
+ const hasKey = json.key !== void 0;
+ const hasLabel = json.label !== void 0;
+ const hasOptions = Array.isArray(json.options);
+ if (!hasKey) console.error('Style attribute is missing the "key" property');
+ if (!hasLabel) console.error('Style attribute is missing the "label" property');
+ if (!hasOptions) console.error('Style attribute is missing the "options" property or it is not an array');
+ json.key = TextHelper.toCamelCase(json.key);
+ let optionsValid = true;
+ let hasDefault = false;
+ for (const option of json.options) {
+ const hasIcon = option.icon !== void 0;
+ const hasLabel2 = option.label !== void 0;
+ const hasValue = option.value !== void 0;
+ if (!hasIcon) console.error(`Style attribute option (${(_a = option.value) != null ? _a : option.label}) is missing the "icon" property`);
+ if (!hasLabel2) console.error(`Style attribute option (${option.value}) is missing the "label" property`);
+ if (!hasValue) console.error(`Style attribute option (${option.label}) is missing the "value" property`);
+ if (!hasIcon || !hasLabel2 || !hasValue) optionsValid = false;
+ if (option.value === null) hasDefault = true;
+ }
+ if (!hasDefault) console.error('Style attribute is missing a default option (option with a "value" of null)');
+ const isValid = hasKey && hasLabel && hasOptions && optionsValid && hasDefault;
+ return isValid ? json : null;
+}
+var BUILTIN_NODE_STYLE_ATTRIBUTES = [
+ {
+ key: "textAlign",
+ label: "Text Alignment",
+ nodeTypes: ["text"],
+ options: [
+ {
+ icon: "align-left",
+ label: "Left",
+ value: null
+ },
+ {
+ icon: "align-center",
+ label: "Center",
+ value: "center"
+ },
+ {
+ icon: "align-right",
+ label: "Right",
+ value: "right"
+ }
+ ]
+ },
+ {
+ key: "shape",
+ label: "Shape",
+ nodeTypes: ["text"],
+ options: [
+ {
+ icon: "rectangle-horizontal",
+ label: "Round Rectangle",
+ value: null
+ },
+ {
+ icon: "shape-pill",
+ label: "Pill",
+ value: "pill"
+ },
+ {
+ icon: "diamond",
+ label: "Diamond",
+ value: "diamond"
+ },
+ {
+ icon: "shape-parallelogram",
+ label: "Parallelogram",
+ value: "parallelogram"
+ },
+ {
+ icon: "circle",
+ label: "Circle",
+ value: "circle"
+ },
+ {
+ icon: "shape-predefined-process",
+ label: "Predefined Process",
+ value: "predefined-process"
+ },
+ {
+ icon: "shape-document",
+ label: "Document",
+ value: "document"
+ },
+ {
+ icon: "shape-database",
+ label: "Database",
+ value: "database"
+ }
+ ]
+ },
+ {
+ key: "border",
+ label: "Border",
+ options: [
+ {
+ icon: "border-solid",
+ label: "Solid",
+ value: null
+ },
+ {
+ icon: "border-dashed",
+ label: "Dashed",
+ value: "dashed"
+ },
+ {
+ icon: "border-dotted",
+ label: "Dotted",
+ value: "dotted"
+ },
+ {
+ icon: "eye-off",
+ label: "Invisible",
+ value: "invisible"
+ }
+ ]
+ }
+];
+var BUILTIN_EDGE_STYLE_ATTRIBUTES = [
+ {
+ key: "path",
+ label: "Path Style",
+ options: [
+ {
+ icon: "path-solid",
+ label: "Solid",
+ value: null
+ },
+ {
+ icon: "path-dotted",
+ label: "Dotted",
+ value: "dotted"
+ },
+ {
+ icon: "path-short-dashed",
+ label: "Short Dashed",
+ value: "short-dashed"
+ },
+ {
+ icon: "path-long-dashed",
+ label: "Long Dashed",
+ value: "long-dashed"
+ }
+ ]
+ },
+ {
+ key: "arrow",
+ label: "Arrow Style",
+ options: [
+ {
+ icon: "arrow-triangle",
+ label: "Triangle",
+ value: null
+ },
+ {
+ icon: "arrow-triangle-outline",
+ label: "Triangle Outline",
+ value: "triangle-outline"
+ },
+ {
+ icon: "arrow-thin-triangle",
+ label: "Thin Triangle",
+ value: "thin-triangle"
+ },
+ {
+ icon: "arrow-halved-triangle",
+ label: "Halved Triangle",
+ value: "halved-triangle"
+ },
+ {
+ icon: "arrow-diamond",
+ label: "Diamond",
+ value: "diamond"
+ },
+ {
+ icon: "arrow-diamond-outline",
+ label: "Diamond Outline",
+ value: "diamond-outline"
+ },
+ {
+ icon: "arrow-circle",
+ label: "Circle",
+ value: "circle"
+ },
+ {
+ icon: "arrow-circle-outline",
+ label: "Circle Outline",
+ value: "circle-outline"
+ },
+ {
+ icon: "tally-1",
+ label: "Blunt",
+ value: "blunt"
+ }
+ ]
+ },
+ {
+ key: "pathfindingMethod",
+ label: "Pathfinding Method",
+ options: [
+ {
+ icon: "pathfinding-method-bezier",
+ label: "Bezier",
+ value: null
+ },
+ {
+ icon: "slash",
+ label: "Direct",
+ value: "direct"
+ },
+ {
+ icon: "pathfinding-method-square",
+ label: "Square",
+ value: "square"
+ },
+ {
+ icon: "map",
+ label: "A*",
+ value: "a-star"
+ }
+ ]
+ }
+];
+
+// src/managers/css-styles-config-manager.ts
+var import_obsidian3 = require("obsidian");
+var CssStylesConfigManager = class {
+ constructor(plugin, trigger, validate) {
+ this.plugin = plugin;
+ this.validate = validate;
+ this.cachedConfig = null;
+ this.configRegex = new RegExp(`\\/\\*\\s*@${trigger}\\s*\\n([\\s\\S]*?)\\*\\/`, "g");
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "css-change",
+ () => {
+ this.cachedConfig = null;
+ }
+ ));
+ }
+ getStyles() {
+ if (this.cachedConfig) return this.cachedConfig;
+ this.cachedConfig = [];
+ const styleSheets = document.styleSheets;
+ for (let i = 0; i < styleSheets.length; i++) {
+ const sheet = styleSheets.item(i);
+ if (!sheet) continue;
+ const styleSheetConfigs = this.parseStyleConfigsFromCSS(sheet);
+ for (const config of styleSheetConfigs) {
+ const validConfig = this.validate(config);
+ if (!validConfig) continue;
+ this.cachedConfig.push(validConfig);
+ }
+ }
+ return this.cachedConfig;
+ }
+ parseStyleConfigsFromCSS(sheet) {
+ var _a, _b;
+ const textContent = (_b = (_a = sheet == null ? void 0 : sheet.ownerNode) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim();
+ if (!textContent) return [];
+ const configs = [];
+ const matches = textContent.matchAll(this.configRegex);
+ for (const match of matches) {
+ const yamlString = match[1];
+ const configYaml = (0, import_obsidian3.parseYaml)(yamlString);
+ configs.push(configYaml);
+ }
+ return configs;
+ }
+};
+
+// src/canvas-extensions/advanced-styles/edge-styles.ts
+var GET_EDGE_CSS_STYLES_MANAGER = (plugin) => new CssStylesConfigManager(plugin, "advanced-canvas-edge-style", styleAttributeValidator);
+var EDGE_PATHFINDING_METHODS = {
+ "direct": EdgePathfindingDirect,
+ "square": EdgePathfindingSquare,
+ "a-star": EdgePathfindingAStar
+};
+var MAX_LIVE_UPDATE_SELECTION_SIZE = 5;
+var EdgeStylesExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "edgesStylingFeatureEnabled";
+ }
+ init() {
+ this.cssStylesManager = GET_EDGE_CSS_STYLES_MANAGER(this.plugin);
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-changed",
+ (canvas, edge) => this.onEdgeChanged(canvas, edge)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-center-requested",
+ (canvas, edge, center) => this.onEdgeCenterRequested(canvas, edge, center)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-added",
+ (canvas, node) => {
+ if (canvas.dirty.size > 1 && !canvas.isPasting) return;
+ this.updateAllEdgesInArea(canvas, node.getBBox());
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-moved",
+ // Only update edges this way if a node got moved with the arrow keys
+ (canvas, node, keyboard) => node.initialized && keyboard ? this.updateAllEdgesInArea(canvas, node.getBBox()) : void 0
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-removed",
+ (canvas, node) => this.updateAllEdgesInArea(canvas, node.getBBox())
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:dragging-state-changed",
+ (canvas, isDragging) => {
+ if (isDragging) return;
+ const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0);
+ const selectedNodeBBoxes = selectedNodes.map((node) => node.getBBox());
+ const selectedNodeBBox = BBoxHelper.combineBBoxes(selectedNodeBBoxes);
+ this.updateAllEdgesInArea(canvas, selectedNodeBBox);
+ }
+ ));
+ }
+ // Skip if isDragging and setting isn't enabled and not connecting an edge
+ shouldUpdateEdge(canvas) {
+ return !canvas.isDragging || this.plugin.settings.getSetting("edgeStyleUpdateWhileDragging") || canvas.canvasEl.hasClass("is-connecting");
+ }
+ onPopupMenuCreated(canvas) {
+ var _a;
+ const selectedEdges = [...canvas.selection].filter((item) => item.path !== void 0);
+ if (canvas.readonly || selectedEdges.length === 0 || selectedEdges.length !== canvas.selection.size)
+ return;
+ CanvasHelper.addStyleAttributesToPopup(
+ this.plugin,
+ canvas,
+ [
+ ...BUILTIN_EDGE_STYLE_ATTRIBUTES,
+ /* Legacy */
+ ...this.plugin.settings.getSetting("customEdgeStyleAttributes"),
+ ...this.cssStylesManager.getStyles()
+ ],
+ (_a = selectedEdges[0].getData().styleAttributes) != null ? _a : {},
+ (attribute, value) => this.setStyleAttributeForSelection(canvas, attribute, value)
+ );
+ }
+ setStyleAttributeForSelection(canvas, attribute, value) {
+ const selectedEdges = [...canvas.selection].filter((item) => item.path !== void 0);
+ for (const edge of selectedEdges) {
+ const edgeData = edge.getData();
+ edge.setData({
+ ...edgeData,
+ styleAttributes: {
+ ...edgeData.styleAttributes,
+ [attribute.key]: value
+ }
+ });
+ }
+ canvas.pushHistory(canvas.getData());
+ }
+ updateAllEdgesInArea(canvas, bbox) {
+ if (!this.shouldUpdateEdge(canvas)) return;
+ for (const edge of canvas.edges.values()) {
+ if (!BBoxHelper.isColliding(edge.getBBox(), bbox)) continue;
+ canvas.markDirty(edge);
+ }
+ }
+ onEdgeChanged(canvas, edge) {
+ var _a, _b, _c, _d, _e, _f, _g;
+ if (!canvas.dirty.has(edge) && !canvas.selection.has(edge)) return;
+ if (!this.shouldUpdateEdge(canvas)) {
+ const tooManySelected = canvas.selection.size > MAX_LIVE_UPDATE_SELECTION_SIZE;
+ if (tooManySelected) return;
+ const groupNodesSelected = [...canvas.selection].some((item) => {
+ var _a2;
+ return ((_a2 = item.getData()) == null ? void 0 : _a2.type) === "group";
+ });
+ if (groupNodesSelected) return;
+ }
+ const edgeData = edge.getData();
+ if (!edge.bezier) return;
+ edge.center = void 0;
+ edge.updatePath();
+ const pathfindingMethod = (_a = edgeData.styleAttributes) == null ? void 0 : _a.pathfindingMethod;
+ if (pathfindingMethod && pathfindingMethod in EDGE_PATHFINDING_METHODS) {
+ const fromNodeBBox = edge.from.node.getBBox();
+ const fromBBoxSidePos = BBoxHelper.getCenterOfBBoxSide(fromNodeBBox, edge.from.side);
+ const fromPos = edge.from.end === "none" ? fromBBoxSidePos : edge.bezier.from;
+ const toNodeBBox = edge.to.node.getBBox();
+ const toBBoxSidePos = BBoxHelper.getCenterOfBBoxSide(toNodeBBox, edge.to.side);
+ const toPos = edge.to.end === "none" ? toBBoxSidePos : edge.bezier.to;
+ const path = new EDGE_PATHFINDING_METHODS[pathfindingMethod](
+ this.plugin,
+ canvas,
+ fromNodeBBox,
+ fromPos,
+ fromBBoxSidePos,
+ edge.from.side,
+ toNodeBBox,
+ toPos,
+ toBBoxSidePos,
+ edge.to.side
+ ).getPath();
+ if (!path) return;
+ edge.center = path.center;
+ edge.path.interaction.setAttr("d", path == null ? void 0 : path.svgPath);
+ edge.path.display.setAttr("d", path == null ? void 0 : path.svgPath);
+ }
+ (_b = edge.labelElement) == null ? void 0 : _b.render();
+ const arrowPolygonPoints = this.getArrowPolygonPoints((_c = edgeData.styleAttributes) == null ? void 0 : _c.arrow);
+ if ((_d = edge.fromLineEnd) == null ? void 0 : _d.el) (_e = edge.fromLineEnd.el.querySelector("polygon")) == null ? void 0 : _e.setAttribute("points", arrowPolygonPoints);
+ if ((_f = edge.toLineEnd) == null ? void 0 : _f.el) (_g = edge.toLineEnd.el.querySelector("polygon")) == null ? void 0 : _g.setAttribute("points", arrowPolygonPoints);
+ }
+ onEdgeCenterRequested(_canvas, edge, center) {
+ var _a, _b, _c, _d;
+ center.x = (_b = (_a = edge.center) == null ? void 0 : _a.x) != null ? _b : center.x;
+ center.y = (_d = (_c = edge.center) == null ? void 0 : _c.y) != null ? _d : center.y;
+ }
+ getArrowPolygonPoints(arrowStyle) {
+ if (arrowStyle === "halved-triangle")
+ return `-2,0 7.5,12 -2,12`;
+ else if (arrowStyle === "thin-triangle")
+ return `0,0 7,10 0,0 0,10 0,0 -7,10`;
+ else if (arrowStyle === "diamond" || arrowStyle === "diamond-outline")
+ return `0,0 5,10 0,20 -5,10`;
+ else if (arrowStyle === "circle" || arrowStyle === "circle-outline")
+ return `0 0, 4.95 1.8, 7.5 6.45, 6.6 11.7, 2.7 15, -2.7 15, -6.6 11.7, -7.5 6.45, -4.95 1.8`;
+ else if (arrowStyle === "blunt")
+ return `-10,8 10,8 10,6 -10,6`;
+ else
+ return `0,0 6.5,10.4 -6.5,10.4`;
+ }
+};
+
+// src/canvas-extensions/advanced-styles/node-styles.ts
+var GET_NODE_CSS_STYLES_MANAGER = (plugin) => new CssStylesConfigManager(plugin, "advanced-canvas-node-style", styleAttributeValidator);
+var NodeStylesExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "nodeStylingFeatureEnabled";
+ }
+ init() {
+ this.cssStylesManager = GET_NODE_CSS_STYLES_MANAGER(this.plugin);
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ }
+ onPopupMenuCreated(canvas) {
+ var _a;
+ const selectionNodeData = canvas.getSelectionData().nodes;
+ if (canvas.readonly || selectionNodeData.length === 0 || selectionNodeData.length !== canvas.selection.size)
+ return;
+ const selectedNodeTypes = new Set(selectionNodeData.map((node) => node.type));
+ const availableNodeStyles = [
+ ...BUILTIN_NODE_STYLE_ATTRIBUTES,
+ /* Legacy */
+ ...this.plugin.settings.getSetting("customNodeStyleAttributes"),
+ ...this.cssStylesManager.getStyles()
+ ].filter((style) => !style.nodeTypes || style.nodeTypes.some((type) => selectedNodeTypes.has(type)));
+ CanvasHelper.addStyleAttributesToPopup(
+ this.plugin,
+ canvas,
+ availableNodeStyles,
+ (_a = selectionNodeData[0].styleAttributes) != null ? _a : {},
+ (attribute, value) => this.setStyleAttributeForSelection(canvas, attribute, value)
+ );
+ }
+ setStyleAttributeForSelection(canvas, attribute, value) {
+ const selectionNodeData = canvas.getSelectionData().nodes;
+ for (const nodeData of selectionNodeData) {
+ const node = canvas.nodes.get(nodeData.id);
+ if (!node) continue;
+ if (attribute.nodeTypes && !attribute.nodeTypes.includes(nodeData.type)) continue;
+ node.setData({
+ ...nodeData,
+ styleAttributes: {
+ ...nodeData.styleAttributes,
+ [attribute.key]: value
+ }
+ });
+ }
+ canvas.pushHistory(canvas.getData());
+ }
+};
+
+// src/canvas-extensions/variable-breakpoint-canvas-extension.ts
+var VARIABLE_BREAKPOINT_CSS_VAR = "--variable-breakpoint";
+var VariableBreakpointCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "variableBreakpointFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-breakpoint-changed",
+ (canvas, node, breakpointRef) => this.onNodeBreakpointChanged(canvas, node, breakpointRef)
+ ));
+ }
+ onNodeBreakpointChanged(canvas, node, breakpointRef) {
+ if (!node.initialized) return;
+ if (node.breakpoint === void 0) {
+ const computedStyle = window.getComputedStyle(node.nodeEl);
+ const variableBreakpointString = computedStyle.getPropertyValue(VARIABLE_BREAKPOINT_CSS_VAR);
+ let numberBreakpoint;
+ if (variableBreakpointString.length > 0 && !isNaN(numberBreakpoint = parseFloat(variableBreakpointString)))
+ node.breakpoint = numberBreakpoint;
+ else node.breakpoint = null;
+ }
+ if (node.breakpoint === null) return;
+ breakpointRef.value = canvas.zoom > node.breakpoint;
+ }
+};
+
+// src/settings.ts
+var README_URL = "https://github.com/Developer-Mike/obsidian-advanced-canvas?tab=readme-ov-file";
+var SPENT_HOURS = 250;
+var RECEIVED_DONATIONS = 606;
+var HOURLY_RATE_GOAL = 15;
+var KOFI_PAGE_URL = "https://ko-fi.com/X8X27IA08";
+var KOFI_BADGE_URI = "data:image/webp;base64,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";
+var DEFAULT_SETTINGS_VALUES = {
+ nodeTypeOnDoubleClick: "text",
+ alignNewNodesToGrid: true,
+ defaultTextNodeDimensions: [260, 60],
+ defaultFileNodeDimensions: [400, 400],
+ minNodeSize: 60,
+ maxNodeWidth: -1,
+ disableFontSizeRelativeToZoom: false,
+ canvasMetadataCompatibilityEnabled: true,
+ enableSingleNodeLinks: true,
+ combineCustomStylesInDropdown: false,
+ nodeStylingFeatureEnabled: true,
+ customNodeStyleAttributes: [],
+ defaultTextNodeColor: 0,
+ defaultTextNodeStyleAttributes: {},
+ edgesStylingFeatureEnabled: true,
+ customEdgeStyleAttributes: [],
+ inheritEdgeColorFromNode: false,
+ defaultEdgeColor: 0,
+ defaultEdgeLineDirection: "unidirectional",
+ defaultEdgeStyleAttributes: {},
+ edgeStyleUpdateWhileDragging: false,
+ edgeStyleSquarePathRounded: true,
+ edgeStylePathfinderAllowDiagonal: false,
+ edgeStylePathfinderPathRounded: true,
+ variableBreakpointFeatureEnabled: false,
+ zOrderingControlFeatureEnabled: false,
+ zOrderingControlShowOneLayerShiftOptions: false,
+ aspectRatioControlFeatureEnabled: false,
+ commandsFeatureEnabled: true,
+ zoomToClonedNode: true,
+ cloneNodeMargin: 20,
+ expandNodeStepSize: 20,
+ nativeFileSearchEnabled: true,
+ floatingEdgeFeatureEnabled: true,
+ allowFloatingEdgeCreation: false,
+ newEdgeFromSideFloating: false,
+ flipEdgeFeatureEnabled: true,
+ betterExportFeatureEnabled: true,
+ betterReadonlyEnabled: false,
+ hideBackgroundGridWhenInReadonly: true,
+ disableNodePopup: false,
+ disableZoom: false,
+ disablePan: false,
+ autoResizeNodeFeatureEnabled: false,
+ autoResizeNodeEnabledByDefault: false,
+ autoResizeNodeMaxHeight: -1,
+ autoResizeNodeSnapToGrid: true,
+ collapsibleGroupsFeatureEnabled: true,
+ collapsedGroupPreviewOnDrag: true,
+ focusModeFeatureEnabled: false,
+ presentationFeatureEnabled: true,
+ showSetStartNodeInPopup: false,
+ defaultSlideDimensions: [1200, 675],
+ wrapInSlidePadding: 20,
+ resetViewportOnPresentationEnd: true,
+ useArrowKeysToChangeSlides: true,
+ usePgUpPgDownKeysToChangeSlides: true,
+ zoomToSlideWithoutPadding: true,
+ useUnclampedZoomWhilePresenting: false,
+ fullscreenPresentationEnabled: true,
+ slideTransitionAnimationDuration: 0.5,
+ slideTransitionAnimationIntensity: 1.25,
+ canvasEncapsulationEnabled: false,
+ portalsFeatureEnabled: true,
+ showEdgesIntoDisabledPortals: true,
+ autoFileNodeEdgesFeatureEnabled: false,
+ autoFileNodeEdgesFrontmatterKey: "canvas-edges",
+ edgeHighlightEnabled: false,
+ highlightIncomingEdges: false,
+ edgeSelectionEnabled: false,
+ selectEdgeByDirection: false
+};
+var SETTINGS = {
+ general: {
+ label: "General",
+ description: "General settings of the Advanced Canvas plugin.",
+ disableToggle: true,
+ children: {
+ nodeTypeOnDoubleClick: {
+ label: "Node type on double click",
+ description: "The type of node that will be created when double clicking on the canvas.",
+ type: "dropdown",
+ options: {
+ "text": "Text",
+ "file": "File"
+ }
+ },
+ alignNewNodesToGrid: {
+ label: "Always align new nodes to grid",
+ description: "When enabled, new nodes will be aligned to the grid.",
+ type: "boolean"
+ },
+ defaultTextNodeDimensions: {
+ label: "Default text node dimensions",
+ description: "The default dimensions of a text node.",
+ type: "dimension",
+ parse: (value) => {
+ const width = Math.max(1, parseInt(value[0]) || 0);
+ const height = Math.max(1, parseInt(value[1]) || 0);
+ return [width, height];
+ }
+ },
+ defaultFileNodeDimensions: {
+ label: "Default file node dimensions",
+ description: "The default dimensions of a file node.",
+ type: "dimension",
+ parse: (value) => {
+ const width = Math.max(1, parseInt(value[0]) || 0);
+ const height = Math.max(1, parseInt(value[1]) || 0);
+ return [width, height];
+ }
+ },
+ minNodeSize: {
+ label: "Minimum node size",
+ description: "The minimum size (either width or height) of a node.",
+ type: "number",
+ parse: (value) => Math.max(1, parseInt(value) || 0)
+ },
+ maxNodeWidth: {
+ label: "Maximum node width",
+ description: "The maximum width of a node. Set to -1 for no limit.",
+ type: "number",
+ parse: (value) => Math.max(-1, parseInt(value) || 0)
+ },
+ disableFontSizeRelativeToZoom: {
+ label: "Disable font size relative to zoom",
+ description: "When enabled, the font size of e.g. group node titles and edge labels will not increase when zooming out.",
+ type: "boolean"
+ }
+ }
+ },
+ commandsFeatureEnabled: {
+ label: "Extended commands",
+ description: "Add more commands to the canvas.",
+ infoSection: "canvas-commands",
+ children: {
+ zoomToClonedNode: {
+ label: "Zoom to cloned node",
+ description: "When enabled, the canvas will zoom to the cloned node.",
+ type: "boolean"
+ },
+ cloneNodeMargin: {
+ label: "Clone node margin",
+ description: "The margin between the cloned node and the source node.",
+ type: "number",
+ parse: (value) => Math.max(0, parseInt(value) || 0)
+ },
+ expandNodeStepSize: {
+ label: "Expand node step size",
+ description: "The step size for expanding the node.",
+ type: "number",
+ parse: (value) => Math.max(1, parseInt(value) || 0)
+ }
+ }
+ },
+ canvasMetadataCompatibilityEnabled: {
+ label: "Enable .canvas metadata cache compatibility",
+ description: "Make .canvas files compatible with the backlinks and outgoing links feature and show the connections in the graph view.",
+ infoSection: "full-metadata-cache-support",
+ children: {
+ enableSingleNodeLinks: {
+ label: "Enable support for linking to a node using a [[wikilink]]",
+ description: "When enabled, you can link and embed a node using [[canvas-file#node-id]].",
+ type: "boolean"
+ }
+ }
+ },
+ nativeFileSearchEnabled: {
+ label: "Native-like file search",
+ description: "When enabled, the file search will be done using the native Obsidian file search.",
+ infoSection: "native-like-file-search",
+ children: {}
+ },
+ autoFileNodeEdgesFeatureEnabled: {
+ label: "Auto file node edges",
+ description: "Automatically create edges between file nodes based their frontmatter links.",
+ infoSection: "auto-file-node-edges",
+ children: {
+ autoFileNodeEdgesFrontmatterKey: {
+ label: "Frontmatter key name",
+ description: "The frontmatter key to fetch the outgoing edges from. (Keep the default to ensure best compatibility.)",
+ type: "text",
+ parse: (value) => value.trim() || "canvas-edges"
+ }
+ }
+ },
+ portalsFeatureEnabled: {
+ label: "Portals",
+ description: "Create portals to other canvases.",
+ infoSection: "portals",
+ children: {
+ showEdgesIntoDisabledPortals: {
+ label: "Show edges into disabled portals",
+ description: "When enabled, edges into disabled portals will be shown.",
+ type: "boolean"
+ }
+ }
+ },
+ collapsibleGroupsFeatureEnabled: {
+ label: "Collapsible groups",
+ description: "Group nodes can be collapsed and expanded to keep the canvas organized.",
+ infoSection: "collapsible-groups",
+ children: {
+ collapsedGroupPreviewOnDrag: {
+ label: "Collapsed group preview on drag",
+ description: "When enabled, a group that is collapsed show its border while dragging a node.",
+ type: "boolean"
+ }
+ }
+ },
+ combineCustomStylesInDropdown: {
+ label: "Combine custom styles",
+ description: "Combine all style attributes of Advanced Canvas in a single dropdown.",
+ children: {}
+ },
+ nodeStylingFeatureEnabled: {
+ label: "Node styling",
+ description: "Style your nodes with different shapes and borders.",
+ infoSection: "node-styles",
+ children: {
+ customNodeStyleAttributes: {
+ label: "Custom node style settings",
+ description: "Add custom style settings for nodes. (Go to GitHub for more information)",
+ type: "button",
+ onClick: () => window.open("https://github.com/Developer-Mike/obsidian-advanced-canvas/blob/main/README.md#custom-styles")
+ },
+ defaultTextNodeColor: {
+ label: "Default text node color",
+ description: "The default color of a text node. The default range is from 0 to 6, where 0 is no color. The range can be extended by using the Custom Colors feature of Advanced Canvas.",
+ type: "number",
+ parse: (value) => Math.max(0, parseInt(value) || 0)
+ },
+ defaultTextNodeStyleAttributes: {
+ label: "Default text node style attributes",
+ type: "styles",
+ getParameters(settingsManager) {
+ return [
+ ...BUILTIN_NODE_STYLE_ATTRIBUTES,
+ /* BUILTINS */
+ ...settingsManager.nodeCssStylesManager.getStyles(),
+ /* CUSTOM CSS STYLES */
+ ...settingsManager.getSetting("customNodeStyleAttributes")
+ /* LEGACY CUSTOM STYLES */
+ ].filter((setting) => {
+ var _a;
+ return setting.nodeTypes === void 0 || ((_a = setting.nodeTypes) == null ? void 0 : _a.includes("text"));
+ });
+ }
+ }
+ }
+ },
+ edgesStylingFeatureEnabled: {
+ label: "Edges styling",
+ description: "Style your edges with different path styles.",
+ infoSection: "edge-styles",
+ children: {
+ customEdgeStyleAttributes: {
+ label: "Custom edge style settings",
+ description: "Add custom style settings for edges. (Go to GitHub for more information)",
+ type: "button",
+ onClick: () => window.open("https://github.com/Developer-Mike/obsidian-advanced-canvas/blob/main/README.md#custom-styles")
+ },
+ inheritEdgeColorFromNode: {
+ label: "Inherit edge color from node",
+ description: "When creating a new edge by dragging from a node, the edge will inherit the color of the node it is dragged from.",
+ type: "boolean"
+ },
+ defaultEdgeColor: {
+ label: "Default edge color",
+ description: "The default color of an edge. The default range is from 0 to 6, where 0 is no color. The range can be extended by using the Custom Colors feature of Advanced Canvas.",
+ type: "number",
+ parse: (value) => Math.max(0, parseInt(value) || 0)
+ },
+ defaultEdgeLineDirection: {
+ label: "Default edge line direction",
+ description: "The default line direction of an edge.",
+ type: "dropdown",
+ options: {
+ "nondirectional": "Nondirectional",
+ "unidirectional": "Unidirectional",
+ "bidirectional": "Bidirectional"
+ }
+ },
+ defaultEdgeStyleAttributes: {
+ label: "Default edge style attributes",
+ type: "styles",
+ getParameters(settingsManager) {
+ return [
+ ...BUILTIN_EDGE_STYLE_ATTRIBUTES,
+ /* BUILTINS */
+ ...settingsManager.edgeCssStylesManager.getStyles(),
+ /* CUSTOM CSS STYLES */
+ ...settingsManager.getSetting("customEdgeStyleAttributes")
+ /* LEGACY CUSTOM STYLES */
+ ];
+ }
+ },
+ edgeStyleUpdateWhileDragging: {
+ label: "Update edge style while dragging (Can be very slow)",
+ description: "When enabled, the edge style will be updated while dragging an edge.",
+ type: "boolean"
+ },
+ edgeStyleSquarePathRounded: {
+ label: "Square path rounded",
+ description: "When enabled, the square path's corners will be rounded.",
+ type: "boolean"
+ },
+ edgeStylePathfinderAllowDiagonal: {
+ label: "A* allow diagonal",
+ description: "When enabled, the A* path style will allow diagonal paths.",
+ type: "boolean"
+ },
+ edgeStylePathfinderPathRounded: {
+ label: "A* rounded path",
+ description: "When enabled, the A* path style will be rounded.",
+ type: "boolean"
+ }
+ }
+ },
+ floatingEdgeFeatureEnabled: {
+ label: "Floating edges (auto edge side)",
+ description: "Floating edges are automatically placed on the most suitable side of the node.",
+ infoSection: "floating-edges-automatic-edge-side",
+ children: {
+ allowFloatingEdgeCreation: {
+ label: "Allow floating edges creation",
+ description: "Allow floating edges creation by dragging the edge over the target node without placing it over a specific side connection point. (If disabled, floating edges can only be created and used by other Advanced Canvas features.)",
+ type: "boolean"
+ },
+ newEdgeFromSideFloating: {
+ label: "New edge from side floating",
+ description: 'When enabled, the "from" side of the edge will always be floating.',
+ type: "boolean"
+ }
+ }
+ },
+ flipEdgeFeatureEnabled: {
+ label: "Flip edges",
+ description: "Flip the direction of edges using the popup menu.",
+ infoSection: "flip-edge",
+ children: {}
+ },
+ presentationFeatureEnabled: {
+ label: "Presentations",
+ description: "Create a presentation from your canvas.",
+ infoSection: "presentation-mode",
+ children: {
+ showSetStartNodeInPopup: {
+ label: 'Show "Set Start Node" in node popup',
+ description: "If turned off, you can still set the start node using the corresponding command.",
+ type: "boolean"
+ },
+ defaultSlideDimensions: {
+ label: "Default slide dimensions",
+ description: "The default dimensions of a slide.",
+ type: "dimension",
+ parse: (value) => {
+ const width = Math.max(1, parseInt(value[0]) || 0);
+ const height = Math.max(1, parseInt(value[1]) || 0);
+ return [width, height];
+ }
+ },
+ wrapInSlidePadding: {
+ label: "Wrap in slide padding",
+ description: "The padding of the slide when wrapping the canvas in a slide.",
+ type: "number",
+ parse: (value) => Math.max(0, parseInt(value) || 0)
+ },
+ resetViewportOnPresentationEnd: {
+ label: "Reset viewport on presentation end",
+ description: "When enabled, the viewport will be reset to the original position after the presentation ends.",
+ type: "boolean"
+ },
+ useArrowKeysToChangeSlides: {
+ label: "Use arrow keys to change slides",
+ description: "When enabled, you can use the arrow keys to change slides in presentation mode.",
+ type: "boolean"
+ },
+ usePgUpPgDownKeysToChangeSlides: {
+ label: "Use PgUp/PgDown keys to change slides",
+ description: "When enabled, you can use the PgUp/PgDown keys to change slides in presentation mode (Makes the presentation mode compatible with most presentation remotes).",
+ type: "boolean"
+ },
+ zoomToSlideWithoutPadding: {
+ label: "Zoom to slide without padding",
+ description: "When enabled, the canvas will zoom to the slide without padding.",
+ type: "boolean"
+ },
+ useUnclampedZoomWhilePresenting: {
+ label: "Use unclamped zoom while presenting",
+ description: "When enabled, the zoom will not be clamped while presenting.",
+ type: "boolean"
+ },
+ fullscreenPresentationEnabled: {
+ label: "Enter fullscreen while presenting",
+ description: "When enabled, presentations automatically request fullscreen. Disable to keep Obsidian windowed during presentations.",
+ type: "boolean"
+ },
+ slideTransitionAnimationDuration: {
+ label: "Slide transition animation duration",
+ description: "The duration of the slide transition animation in seconds. Set to 0 to disable the animation.",
+ type: "number",
+ parse: (value) => Math.max(0, parseFloat(value) || 0)
+ },
+ slideTransitionAnimationIntensity: {
+ label: "Slide transition animation intensity",
+ description: "The intensity of the slide transition animation. The higher the value, the more the canvas will zoom out before zooming in on the next slide.",
+ type: "number",
+ parse: (value) => Math.max(0, parseFloat(value) || 0)
+ }
+ }
+ },
+ zOrderingControlFeatureEnabled: {
+ label: "Z ordering controls",
+ description: "Change the persistent z-index of nodes using the context menu.",
+ children: {
+ zOrderingControlShowOneLayerShiftOptions: {
+ label: "Show one layer shift options",
+ description: "When enabled, you can move nodes one layer forward or backward.",
+ type: "boolean"
+ }
+ }
+ },
+ aspectRatioControlFeatureEnabled: {
+ label: "Aspect ratio control",
+ description: "Change the aspect ratio of nodes using the context menu.",
+ children: {}
+ },
+ variableBreakpointFeatureEnabled: {
+ label: "Variable breakpoint",
+ description: `Change the zoom breakpoint (the zoom level at which the nodes won't render their content anymore) on a per-node basis using the ${VARIABLE_BREAKPOINT_CSS_VAR} CSS variable.`,
+ infoSection: "variable-breakpoints",
+ children: {}
+ },
+ autoResizeNodeFeatureEnabled: {
+ label: "Auto resize node",
+ description: "Automatically resize the height of a node to fit the content.",
+ infoSection: "auto-node-resizing",
+ children: {
+ autoResizeNodeEnabledByDefault: {
+ label: "Enable auto resize by default",
+ description: "When enabled, the auto resize feature will be enabled by default for all nodes.",
+ type: "boolean"
+ },
+ autoResizeNodeMaxHeight: {
+ label: "Max height",
+ description: "The maximum height of the node when auto resizing (-1 for unlimited).",
+ type: "number",
+ parse: (value) => {
+ var _a;
+ return Math.max(-1, (_a = parseInt(value)) != null ? _a : -1);
+ }
+ },
+ autoResizeNodeSnapToGrid: {
+ label: "Snap to grid",
+ description: "When enabled, the height of the node will snap to the grid.",
+ type: "boolean"
+ }
+ }
+ },
+ canvasEncapsulationEnabled: {
+ label: "Canvas encapsulation",
+ description: "Encapsulate a selection of nodes and edges into a new canvas using the context menu.",
+ infoSection: "encapsulate-selection",
+ children: {}
+ },
+ betterReadonlyEnabled: {
+ label: "Better readonly",
+ description: "Improve the readonly mode.",
+ infoSection: "better-readonly",
+ children: {
+ hideBackgroundGridWhenInReadonly: {
+ label: "Hide background grid when in readonly",
+ description: "When enabled, the background grid will be hidden when in readonly mode.",
+ type: "boolean"
+ }
+ }
+ },
+ edgeHighlightEnabled: {
+ label: "Edge highlight",
+ description: "Highlight outgoing (and optionally incoming) edges of a selected node.",
+ infoSection: "edge-highlight",
+ children: {
+ highlightIncomingEdges: {
+ label: "Highlight incoming edges",
+ description: "When enabled, incoming edges will also be highlighted.",
+ type: "boolean"
+ }
+ }
+ },
+ edgeSelectionEnabled: {
+ label: "Edge selection",
+ description: "Select edges connected to the selected node(s) using the popup menu.",
+ infoSection: "edge-selection",
+ children: {
+ selectEdgeByDirection: {
+ label: "Select edge by direction",
+ description: "Select incoming or outgoing edges using separate popup menu items.",
+ type: "boolean"
+ }
+ }
+ },
+ focusModeFeatureEnabled: {
+ label: "Focus mode",
+ description: "Focus on a single node and blur all other nodes.",
+ infoSection: "focus-mode",
+ children: {}
+ }
+};
+var SettingsManager = class {
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.nodeCssStylesManager = GET_NODE_CSS_STYLES_MANAGER(plugin);
+ this.edgeCssStylesManager = GET_EDGE_CSS_STYLES_MANAGER(plugin);
+ }
+ async loadSettings() {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS_VALUES, await this.plugin.loadData());
+ this.plugin.app.workspace.trigger("advanced-canvas:settings-changed");
+ }
+ async saveSettings() {
+ await this.plugin.saveData(this.settings);
+ }
+ getSetting(key) {
+ return this.settings[key];
+ }
+ async setSetting(data) {
+ this.settings = Object.assign(this.settings, data);
+ await this.saveSettings();
+ this.plugin.app.workspace.trigger("advanced-canvas:settings-changed");
+ }
+ addSettingsTab() {
+ this.settingsTab = new AdvancedCanvasPluginSettingTab(this.plugin, this);
+ this.plugin.addSettingTab(this.settingsTab);
+ }
+};
+var AdvancedCanvasPluginSettingTab = class extends import_obsidian4.PluginSettingTab {
+ constructor(plugin, settingsManager) {
+ super(plugin.app, plugin);
+ this.settingsManager = settingsManager;
+ }
+ display() {
+ const { containerEl } = this;
+ containerEl.empty();
+ this.createKofiBanner(containerEl);
+ for (const [headingId, heading] of Object.entries(SETTINGS)) {
+ this.createFeatureHeading(
+ containerEl,
+ heading.label,
+ heading.description,
+ heading.infoSection,
+ heading.disableToggle ? null : headingId
+ );
+ const settingsHeaderChildrenContainerEl = document.createElement("div");
+ settingsHeaderChildrenContainerEl.classList.add("settings-header-children");
+ settingsHeaderChildrenContainerEl.appendChild(document.createElement("span"));
+ containerEl.appendChild(settingsHeaderChildrenContainerEl);
+ for (const [settingId, setting] of Object.entries(heading.children)) {
+ if (!(settingId in DEFAULT_SETTINGS_VALUES)) continue;
+ switch (setting.type) {
+ case "text":
+ this.createTextSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "number":
+ this.createNumberSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "dimension":
+ this.createDimensionSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "boolean":
+ this.createBooleanSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "dropdown":
+ this.createDropdownSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "button":
+ this.createButtonSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ case "styles":
+ this.createStylesSetting(settingsHeaderChildrenContainerEl, settingId, setting);
+ break;
+ }
+ }
+ }
+ }
+ createFeatureHeading(containerEl, label, description, infoSection, settingsKey) {
+ const setting = new import_obsidian4.Setting(containerEl).setHeading().setClass("ac-settings-heading").setName(label).setDesc(description);
+ if (infoSection !== void 0) {
+ setting.addExtraButton(
+ (button) => button.setTooltip("Open github documentation").setIcon("info").onClick(async () => {
+ window.open(`${README_URL}#${infoSection}`);
+ })
+ );
+ }
+ if (settingsKey !== null) {
+ setting.addToggle(
+ (toggle) => toggle.setTooltip("Requires a reload to take effect.").setValue(this.settingsManager.getSetting(settingsKey)).onChange(async (value) => {
+ await this.settingsManager.setSetting({ [settingsKey]: value });
+ new import_obsidian4.Notice("Reload obsidian to apply the changes.");
+ })
+ );
+ }
+ return setting;
+ }
+ createTextSetting(containerEl, settingId, setting) {
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText(
+ (text) => text.setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => {
+ await this.settingsManager.setSetting({ [settingId]: setting.parse ? setting.parse(value) : value });
+ })
+ );
+ }
+ createNumberSetting(containerEl, settingId, setting) {
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText(
+ (text) => text.setValue(this.settingsManager.getSetting(settingId).toString()).onChange(async (value) => {
+ await this.settingsManager.setSetting({ [settingId]: setting.parse(value) });
+ })
+ );
+ }
+ createDimensionSetting(containerEl, settingId, setting) {
+ let text1;
+ let text2;
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText((text) => {
+ text1 = text.setValue(this.settingsManager.getSetting(settingId)[0].toString()).onChange(async (value) => await this.settingsManager.setSetting({ [settingId]: setting.parse([value, text2.getValue()]) }));
+ }).addText((text) => {
+ text2 = text.setValue(this.settingsManager.getSetting(settingId)[1].toString()).onChange(async (value) => await this.settingsManager.setSetting({ [settingId]: setting.parse([text1.getValue(), value]) }));
+ });
+ }
+ createBooleanSetting(containerEl, settingId, setting) {
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addToggle(
+ (toggle) => toggle.setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => {
+ await this.settingsManager.setSetting({ [settingId]: value });
+ })
+ );
+ }
+ createDropdownSetting(containerEl, settingId, setting) {
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addDropdown(
+ (dropdown) => dropdown.addOptions(setting.options).setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => {
+ await this.settingsManager.setSetting({ [settingId]: value });
+ })
+ );
+ }
+ createButtonSetting(containerEl, settingId, setting) {
+ new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addButton(
+ (button) => button.setButtonText("Open").onClick(() => setting.onClick())
+ );
+ }
+ createStylesSetting(containerEl, settingId, setting) {
+ const nestedContainerEl = document.createElement("details");
+ nestedContainerEl.classList.add("setting-item");
+ containerEl.appendChild(nestedContainerEl);
+ const summaryEl = document.createElement("summary");
+ summaryEl.textContent = setting.label;
+ nestedContainerEl.appendChild(summaryEl);
+ for (const styleAttribute of setting.getParameters(this.settingsManager)) {
+ new import_obsidian4.Setting(nestedContainerEl).setName(styleAttribute.label).addDropdown(
+ (dropdown) => {
+ var _a;
+ return dropdown.addOptions(Object.fromEntries(styleAttribute.options.map((option) => [option.value, option.value === null ? `${option.label} (default)` : option.label]))).setValue((_a = this.settingsManager.getSetting(settingId)[styleAttribute.key]) != null ? _a : "null").onChange(async (value) => {
+ const newValue = this.settingsManager.getSetting(settingId);
+ if (value === "null") delete newValue[styleAttribute.key];
+ else newValue[styleAttribute.key] = value;
+ await this.settingsManager.setSetting({
+ [settingId]: newValue
+ });
+ });
+ }
+ );
+ }
+ }
+ async createKofiBanner(containerEl) {
+ const banner = document.createElement("div");
+ banner.classList.add("kofi-banner");
+ const title = document.createElement("h1");
+ title.textContent = "Enjoying the plugin?";
+ banner.appendChild(title);
+ const description = document.createElement("p");
+ description.innerHTML = `
+ Currently, Advanced Canvas has received about ${RECEIVED_DONATIONS}\xA3 in donations with a total of about ${SPENT_HOURS} hours spent on development.
+
+ Please help me develop this plugin further by reaching the goal of ${HOURLY_RATE_GOAL}\xA3/hour \u2764\uFE0F
+ `;
+ banner.appendChild(description);
+ const progressContainer = document.createElement("div");
+ progressContainer.classList.add("progress-container");
+ const progressbar = document.createElement("progress");
+ progressbar.value = RECEIVED_DONATIONS / SPENT_HOURS;
+ progressbar.max = HOURLY_RATE_GOAL;
+ progressContainer.appendChild(progressbar);
+ const hourlyRate = document.createElement("span");
+ hourlyRate.classList.add("hourly-rate");
+ hourlyRate.textContent = `${(RECEIVED_DONATIONS / SPENT_HOURS).toString()}\xA3/h`;
+ progressContainer.appendChild(hourlyRate);
+ banner.appendChild(progressContainer);
+ const koFiButton = document.createElement("a");
+ koFiButton.classList.add("ac-kofi-button");
+ koFiButton.href = KOFI_PAGE_URL;
+ koFiButton.target = "_blank";
+ const koFiImage = document.createElement("img");
+ koFiImage.src = KOFI_BADGE_URI;
+ koFiButton.appendChild(koFiImage);
+ banner.appendChild(koFiButton);
+ containerEl.appendChild(banner);
+ }
+};
+
+// src/managers/windows-manager.ts
+var WindowsManager = class {
+ constructor(plugin) {
+ this.windows = [window];
+ this.plugin = plugin;
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "window-open",
+ (_win, window2) => this.windows.push(window2)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "window-close",
+ (_win, window2) => this.windows = this.windows.filter((w) => w !== window2)
+ ));
+ }
+};
+
+// src/patchers/canvas-patcher.ts
+var import_view = require("@codemirror/view");
+
+// node_modules/monkey-around/mjs/index.js
+function around(obj, factories) {
+ const removers = Object.keys(factories).map((key) => around1(obj, key, factories[key]));
+ return removers.length === 1 ? removers[0] : function() {
+ removers.forEach((r) => r());
+ };
+}
+function around1(obj, method, createWrapper) {
+ const original = obj[method], hadOwn = obj.hasOwnProperty(method);
+ let current = createWrapper(original);
+ if (original)
+ Object.setPrototypeOf(current, original);
+ Object.setPrototypeOf(wrapper, current);
+ obj[method] = wrapper;
+ return remove;
+ function wrapper(...args) {
+ if (current === original && obj[method] === wrapper)
+ remove();
+ return current.apply(this, args);
+ }
+ function remove() {
+ if (obj[method] === wrapper) {
+ if (hadOwn)
+ obj[method] = original;
+ else
+ delete obj[method];
+ }
+ if (current === original)
+ return;
+ current = original;
+ Object.setPrototypeOf(wrapper, original || Function);
+ }
+}
+
+// src/patchers/canvas-patcher.ts
+var import_obsidian5 = require("obsidian");
+
+// node_modules/tiny-jsonc/dist/index.js
+var stringOrCommentRe = /("(?:\\?[^])*?")|(\/\/.*)|(\/\*[^]*?\*\/)/g;
+var stringOrTrailingCommaRe = /("(?:\\?[^])*?")|(,\s*)(?=]|})/g;
+var JSONC = {
+ parse: (text) => {
+ text = String(text);
+ try {
+ return JSON.parse(text);
+ } catch (e) {
+ return JSON.parse(text.replace(stringOrCommentRe, "$1").replace(stringOrTrailingCommaRe, "$1"));
+ }
+ }
+};
+var dist_default = JSONC;
+
+// src/patchers/patcher.ts
+var Patcher = class _Patcher {
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.patch();
+ }
+ static async waitForMapValueLookup(map, viewType, patch) {
+ return new Promise((resolve) => {
+ const uninstaller = around(map, {
+ [viewType]: (next) => function(...args) {
+ const view = next.call(this, ...args);
+ patch(view);
+ const patchedView = next.call(this, ...args);
+ uninstaller();
+ resolve(patchedView);
+ return patchedView;
+ }
+ });
+ });
+ }
+ static async waitForViewRequest(plugin, viewType, patch) {
+ return this.waitForMapValueLookup(plugin.app.viewRegistry.viewByType, viewType, patch);
+ }
+ static OverrideExisting(fn) {
+ return Object.assign(fn, { __overrideExisting: true });
+ }
+ static patchThisAndPrototype(plugin, object, patches, uninstallers) {
+ _Patcher.patch(plugin, object, patches, false, uninstallers);
+ return _Patcher.patchPrototype(plugin, object, patches, uninstallers);
+ }
+ static patchPrototype(plugin, target, patches, uninstallers) {
+ return _Patcher.patch(plugin, target, patches, true, uninstallers);
+ }
+ static patch(plugin, object, patches, prototype = false, uninstallers) {
+ if (!object) return null;
+ const target = prototype ? object.constructor.prototype : object;
+ for (const key of Object.keys(patches)) {
+ const patch = patches[key];
+ if (patch == null ? void 0 : patch.__overrideExisting) {
+ if (typeof target[key] !== "function")
+ throw new Error(`Method ${String(key)} does not exist on target`);
+ }
+ }
+ const uninstaller = around(target, patches);
+ if (uninstallers) uninstallers.push(uninstaller);
+ plugin.register(uninstaller);
+ return object;
+ }
+ static async patchOnce(plugin, object, patches) {
+ const uninstallers = [];
+ const value = await new Promise(
+ (resolve) => this.patch(plugin, object, patches(resolve), false, uninstallers)
+ );
+ for (const uninstall of uninstallers) uninstall();
+ return value;
+ }
+ static tryPatchWorkspacePrototype(plugin, getTarget, patches, uninstallers) {
+ return new Promise((resolve) => {
+ const result = _Patcher.patchPrototype(plugin, getTarget(), patches, uninstallers);
+ if (result) {
+ resolve(result);
+ return;
+ }
+ const listener = plugin.app.workspace.on("layout-change", () => {
+ const result2 = _Patcher.patchPrototype(plugin, getTarget(), patches, uninstallers);
+ if (result2) {
+ plugin.app.workspace.offref(listener);
+ resolve(result2);
+ }
+ });
+ plugin.registerEvent(listener);
+ });
+ }
+};
+
+// src/utils/migration-helper.ts
+var CURRENT_SPEC_VERSION = "1.0-1.0";
+var _MigrationHelper = class _MigrationHelper {
+ static needsMigration(canvas) {
+ var _a;
+ return ((_a = canvas.metadata) == null ? void 0 : _a.version) !== CURRENT_SPEC_VERSION;
+ }
+ static migrate(canvas) {
+ var _a, _b;
+ let version = (_b = (_a = canvas.metadata) == null ? void 0 : _a.version) != null ? _b : "undefined";
+ if (version === CURRENT_SPEC_VERSION) return canvas;
+ while (version !== CURRENT_SPEC_VERSION) {
+ const migrationFunction = _MigrationHelper.MIGRATIONS[version];
+ if (!migrationFunction) {
+ console.error(`No migration function found for version ${version}. Critical error!`);
+ break;
+ }
+ const { version: newVersion, canvas: migratedCanvas } = migrationFunction(canvas);
+ version = newVersion;
+ canvas = migratedCanvas;
+ if (!canvas.metadata) canvas.metadata = { version, frontmatter: {} };
+ else canvas.metadata.version = version;
+ }
+ return canvas;
+ }
+};
+_MigrationHelper.MIGRATIONS = {
+ undefined: (canvas) => {
+ var _a, _b, _c;
+ const TARGET_SPEC_VERSION = "1.0-1.0";
+ let startNode;
+ const globalInterdimensionalEdges = {};
+ for (const node of (_a = canvas.nodes) != null ? _a : []) {
+ node.dynamicHeight = node.autoResizeHeight;
+ delete node.autoResizeHeight;
+ node.ratio = node.sideRatio;
+ delete node.sideRatio;
+ node.collapsed = node.isCollapsed;
+ delete node.isCollapsed;
+ if (node.portalToFile) {
+ node.portal = true;
+ delete node.portalToFile;
+ }
+ if (node.isStartNode) {
+ startNode = node.id;
+ delete node.isStartNode;
+ }
+ if (node.edgesToNodeFromPortal) {
+ const edgesToNodeFromPortal = node.edgesToNodeFromPortal;
+ for (const [portalId, edges] of Object.entries(edgesToNodeFromPortal)) {
+ if (!(portalId in globalInterdimensionalEdges)) globalInterdimensionalEdges[portalId] = [];
+ for (const edge of edges) {
+ if (edge.fromNode !== node.id) edge.fromNode = `${portalId}-${edge.fromNode}`;
+ if (edge.toNode !== node.id) edge.toNode = `${portalId}-${edge.toNode}`;
+ }
+ globalInterdimensionalEdges[portalId].push(...edges);
+ }
+ delete node.edgesToNodeFromPortal;
+ }
+ }
+ for (const node of (_b = canvas.nodes) != null ? _b : []) {
+ if (!(node.id in globalInterdimensionalEdges)) continue;
+ node.interdimensionalEdges = globalInterdimensionalEdges[node.id];
+ }
+ (_c = canvas.metadata) != null ? _c : canvas.metadata = {
+ version: TARGET_SPEC_VERSION,
+ frontmatter: {},
+ startNode
+ };
+ return { version: TARGET_SPEC_VERSION, canvas };
+ }
+};
+var MigrationHelper = _MigrationHelper;
+
+// src/patchers/canvas-patcher.ts
+var CanvasPatcher = class extends Patcher {
+ async patch() {
+ const loadedCanvasViewLeafs = this.plugin.app.workspace.getLeavesOfType("canvas").filter((leaf) => !(0, import_obsidian5.requireApiVersion)("1.7.2") || !leaf.isDeferred);
+ if (loadedCanvasViewLeafs.length > 0) {
+ console.debug(`Patching and reloading loaded canvas views (Count: ${loadedCanvasViewLeafs.length})`);
+ this.patchCanvas(loadedCanvasViewLeafs.first().view);
+ for (const leaf of loadedCanvasViewLeafs) leaf.rebuildView();
+ } else {
+ await Patcher.waitForViewRequest(this.plugin, "canvas", (view) => this.patchCanvas(view));
+ console.debug(`Patched canvas view on first request`);
+ }
+ }
+ patchCanvas(view) {
+ const that = this;
+ Patcher.patchPrototype(this.plugin, view, {
+ setEphemeralState: Patcher.OverrideExisting((next) => function(state) {
+ var _a, _b, _c;
+ if (state == null ? void 0 : state.subpath) {
+ const nodeId = state.subpath.replace(/^#/, "");
+ const node = this.canvas.nodes.get(nodeId);
+ if (node) {
+ this.canvas.selectOnly(node);
+ this.canvas.zoomToSelection();
+ return;
+ }
+ }
+ if (((_b = (_a = state.match) == null ? void 0 : _a.matches) == null ? void 0 : _b[0]) && !((_c = state.match) == null ? void 0 : _c.nodeId)) {
+ const match = state.match.matches[0];
+ const elementType = match[0] === 0 ? "nodes" : "edges";
+ const elementIndex = match[1];
+ const element = elementType === "nodes" ? Array.from(this.canvas.nodes.values())[elementIndex] : Array.from(this.canvas.edges.values())[elementIndex];
+ if (element) {
+ this.canvas.selectOnly(element);
+ this.canvas.zoomToSelection();
+ return;
+ }
+ }
+ return next.call(this, state);
+ }),
+ setViewData: Patcher.OverrideExisting((next) => function(json, ...args) {
+ json = json !== "" ? json : '{"nodes": [], "edges": []}';
+ try {
+ const canvasData = dist_default.parse(json);
+ if (MigrationHelper.needsMigration(canvasData)) {
+ if (this.file) that.plugin.createFileSnapshot(this.file.path, json);
+ json = JSON.stringify(MigrationHelper.migrate(canvasData));
+ }
+ } catch (e) {
+ console.error("Failed to migrate canvas data:", e);
+ }
+ let result;
+ try {
+ result = next.call(this, json, ...args);
+ } catch (e) {
+ console.error("Invalid JSON, repairing through Advanced Canvas:", e);
+ if (this.file) that.plugin.createFileSnapshot(this.file.path, json);
+ json = JSON.stringify(dist_default.parse(json), null, 2);
+ result = next.call(this, json, ...args);
+ }
+ that.plugin.app.workspace.trigger("advanced-canvas:canvas-changed", this.canvas);
+ return result;
+ }),
+ close: Patcher.OverrideExisting((next) => function(...args) {
+ that.plugin.app.workspace.trigger("advanced-canvas:canvas-view-unloaded:before", this);
+ return next.call(this, ...args);
+ })
+ });
+ Patcher.patchPrototype(this.plugin, view.canvas, {
+ markViewportChanged: Patcher.OverrideExisting((next) => function(...args) {
+ that.plugin.app.workspace.trigger("advanced-canvas:viewport-changed:before", this);
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:viewport-changed:after", this);
+ return result;
+ }),
+ markMoved: Patcher.OverrideExisting((next) => function(node) {
+ const result = next.call(this, node);
+ if (!this.viewportChanged) {
+ if (node.prevX !== node.x || node.prevY !== node.y)
+ that.plugin.app.workspace.trigger("advanced-canvas:node-moved", this, node, !this.isDragging);
+ if (node.prevWidth !== node.width || node.prevHeight !== node.height)
+ that.plugin.app.workspace.trigger("advanced-canvas:node-resized", this, node);
+ }
+ node.prevX = node.x;
+ node.prevY = node.y;
+ node.prevWidth = node.width;
+ node.prevHeight = node.height;
+ return result;
+ }),
+ onDoubleClick: Patcher.OverrideExisting((next) => function(event) {
+ const preventDefault = { value: false };
+ that.plugin.app.workspace.trigger("advanced-canvas:double-click", this, event, preventDefault);
+ if (!preventDefault.value) next.call(this, event);
+ }),
+ setDragging: Patcher.OverrideExisting((next) => function(dragging) {
+ const result = next.call(this, dragging);
+ that.plugin.app.workspace.trigger("advanced-canvas:dragging-state-changed", this, dragging);
+ return result;
+ }),
+ // OBSIDIAN-FIX
+ cloneData: Patcher.OverrideExisting((next) => function(elements, shift) {
+ const result = next.call(this, elements, shift);
+ elements.nodes = elements.nodes.map((nodeData) => JSON.parse(JSON.stringify(nodeData)));
+ elements.edges = elements.edges.map((edgeData) => JSON.parse(JSON.stringify(edgeData)));
+ return result;
+ }),
+ getContainingNodes: Patcher.OverrideExisting((next) => function(bbox) {
+ const result = next.call(this, bbox);
+ that.plugin.app.workspace.trigger("advanced-canvas:containing-nodes-requested", this, bbox, result);
+ return result;
+ }),
+ updateSelection: Patcher.OverrideExisting((next) => function(update) {
+ const oldSelection = new Set(this.selection);
+ const result = next.call(this, update);
+ that.plugin.app.workspace.trigger("advanced-canvas:selection-changed", this, oldSelection, (update2) => next.call(this, update2));
+ return result;
+ }),
+ createTextNode: Patcher.OverrideExisting((next) => function(...args) {
+ const node = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node);
+ return node;
+ }),
+ createFileNode: Patcher.OverrideExisting((next) => function(...args) {
+ const node = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node);
+ return node;
+ }),
+ createFileNodes: Patcher.OverrideExisting((next) => function(...args) {
+ const nodes = next.call(this, ...args);
+ nodes.forEach((node) => that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node));
+ return nodes;
+ }),
+ createGroupNode: Patcher.OverrideExisting((next) => function(...args) {
+ const node = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node);
+ return node;
+ }),
+ createLinkNode: Patcher.OverrideExisting((next) => function(...args) {
+ const node = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node);
+ return node;
+ }),
+ addNode: Patcher.OverrideExisting((next) => function(node) {
+ that.patchNode(node);
+ return next.call(this, node);
+ }),
+ addEdge: Patcher.OverrideExisting((next) => function(edge) {
+ that.patchEdge(edge);
+ if (!this.viewportChanged) that.plugin.app.workspace.trigger("advanced-canvas:edge-created", this, edge);
+ return next.call(this, edge);
+ }),
+ removeNode: Patcher.OverrideExisting((next) => function(node) {
+ const result = next.call(this, node);
+ if (!this.isClearing) that.plugin.app.workspace.trigger("advanced-canvas:node-removed", this, node);
+ return result;
+ }),
+ removeEdge: Patcher.OverrideExisting((next) => function(edge) {
+ const result = next.call(this, edge);
+ if (!this.isClearing) that.plugin.app.workspace.trigger("advanced-canvas:edge-removed", this, edge);
+ return result;
+ }),
+ handleCopy: Patcher.OverrideExisting((next) => function(...args) {
+ this.isCopying = true;
+ const result = next.call(this, ...args);
+ this.isCopying = false;
+ return result;
+ }),
+ handlePaste: Patcher.OverrideExisting((next) => function(...args) {
+ this.isPasting = true;
+ const result = next.call(this, ...args);
+ this.isPasting = false;
+ return result;
+ }),
+ getSelectionData: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ if (this.isCopying) that.plugin.app.workspace.trigger("advanced-canvas:copy", this, result);
+ return result;
+ }),
+ zoomToBbox: Patcher.OverrideExisting((next) => function(bbox) {
+ that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:before", this, bbox);
+ const result = next.call(this, bbox);
+ that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:after", this, bbox);
+ return result;
+ }),
+ // Custom
+ zoomToRealBbox: (_next) => function(bbox) {
+ if (this.canvasRect.width === 0 || this.canvasRect.height === 0) return;
+ that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:before", this, bbox);
+ const widthZoom = this.canvasRect.width / (bbox.maxX - bbox.minX);
+ const heightZoom = this.canvasRect.height / (bbox.maxY - bbox.minY);
+ const zoom = this.screenshotting ? Math.min(widthZoom, heightZoom) : Math.clamp(Math.min(widthZoom, heightZoom), -4, 1);
+ this.tZoom = Math.log2(zoom);
+ this.zoomCenter = null;
+ this.tx = (bbox.minX + bbox.maxX) / 2;
+ this.ty = (bbox.minY + bbox.maxY) / 2;
+ this.markViewportChanged();
+ that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:after", this, bbox);
+ },
+ setReadonly: Patcher.OverrideExisting((next) => function(readonly) {
+ const result = next.call(this, readonly);
+ that.plugin.app.workspace.trigger("advanced-canvas:readonly-changed", this, readonly);
+ return result;
+ }),
+ undo: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ this.importData(this.getData(), true);
+ that.plugin.app.workspace.trigger("advanced-canvas:undo", this);
+ return result;
+ }),
+ redo: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ this.importData(this.getData(), true);
+ that.plugin.app.workspace.trigger("advanced-canvas:redo", this);
+ return result;
+ }),
+ clear: Patcher.OverrideExisting((next) => function(...args) {
+ this.isClearing = true;
+ const result = next.call(this, ...args);
+ this.isClearing = false;
+ return result;
+ }),
+ /*setData: Patcher.OverrideExisting(next => function (...args: any): void {
+ //
+ const result = next.call(this, ...args)
+ //
+ return result
+ }),*/
+ getData: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:data-requested", this, result);
+ return result;
+ }),
+ importData: Patcher.OverrideExisting((next) => function(data, clearCanvas, silent) {
+ var _a;
+ const targetFilePath = (_a = this.view.file) == null ? void 0 : _a.path;
+ const setData = (data2) => {
+ if (!this.view.file || this.view.file.path !== targetFilePath) return;
+ this.importData(data2, true, true);
+ };
+ if (!silent) that.plugin.app.workspace.trigger("advanced-canvas:data-loaded:before", this, data, setData);
+ const result = next.call(this, data, clearCanvas);
+ if (!silent) that.plugin.app.workspace.trigger("advanced-canvas:data-loaded:after", this, data, setData);
+ return result;
+ }),
+ requestSave: Patcher.OverrideExisting((next) => function(...args) {
+ that.plugin.app.workspace.trigger("advanced-canvas:canvas-saved:before", this);
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:canvas-saved:after", this);
+ return result;
+ })
+ });
+ Patcher.patchPrototype(this.plugin, view.canvas.menu, {
+ render: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:popup-menu-created", this.canvas);
+ next.call(this);
+ return result;
+ })
+ });
+ Patcher.patchPrototype(this.plugin, view.canvas.nodeInteractionLayer, {
+ setTarget: Patcher.OverrideExisting((next) => function(node) {
+ const result = next.call(this, node);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-interaction", this.canvas, node);
+ return result;
+ })
+ });
+ this.plugin.registerEditorExtension([import_view.EditorView.updateListener.of((update) => {
+ if (!update.docChanged) return;
+ const editor = update.state.field(import_obsidian5.editorInfoField);
+ const node = editor.node;
+ if (!node) return;
+ that.plugin.app.workspace.trigger("advanced-canvas:node-text-content-changed", node.canvas, node, update);
+ })]);
+ }
+ patchNode(node) {
+ const that = this;
+ Patcher.patch(this.plugin, node, {
+ setData: Patcher.OverrideExisting((next) => function(data, addHistory) {
+ const result = next.call(this, data);
+ if (node.initialized && !node.isDirty) {
+ node.isDirty = true;
+ that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, node);
+ delete node.isDirty;
+ }
+ this.canvas.data = this.canvas.getData();
+ if (this.initialized) this.canvas.view.requestSave();
+ if (addHistory) this.canvas.pushHistory(this.canvas.data);
+ return result;
+ }),
+ setZIndex: (_next) => function(value) {
+ this.setData({
+ ...this.getData(),
+ zIndex: value
+ }, true);
+ this.updateZIndex();
+ },
+ updateZIndex: Patcher.OverrideExisting((next) => function() {
+ const persistentZIndex = this.getData().zIndex;
+ if (persistentZIndex === void 0) return next.call(this);
+ this.canvas.zIndexCounter = Math.max(this.canvas.zIndexCounter, persistentZIndex);
+ this.renderZIndex();
+ }),
+ renderZIndex: Patcher.OverrideExisting((next) => function() {
+ const persistentZIndex = this.getData().zIndex;
+ if (persistentZIndex === void 0) return next.call(this);
+ this.zIndex = persistentZIndex;
+ if (this.canvas.selection.size === 1 && this.canvas.selection.has(this))
+ this.zIndex = this.canvas.zIndexCounter + 1;
+ this.nodeEl.style.zIndex = this.zIndex.toString();
+ }),
+ setIsEditing: Patcher.OverrideExisting((next) => function(editing, ...args) {
+ const result = next.call(this, editing, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-editing-state-changed", this.canvas, node, editing);
+ return result;
+ }),
+ updateBreakpoint: Patcher.OverrideExisting((next) => function(breakpoint) {
+ const breakpointRef = { value: breakpoint };
+ that.plugin.app.workspace.trigger("advanced-canvas:node-breakpoint-changed", this.canvas, node, breakpointRef);
+ return next.call(this, breakpointRef.value);
+ }),
+ getBBox: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-bbox-requested", this.canvas, node, result);
+ return result;
+ }),
+ onConnectionPointerdown: Patcher.OverrideExisting((next) => function(e, side) {
+ const addEdgeEventRef = that.plugin.app.workspace.on("advanced-canvas:edge-added", (_canvas, edge) => {
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:before", this.canvas, edge, e, true, "to");
+ that.plugin.app.workspace.offref(addEdgeEventRef);
+ document.addEventListener("pointerup", (e2) => {
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:after", this.canvas, edge, e2, true, "to");
+ }, { once: true });
+ });
+ const result = next.call(this, e, side);
+ return result;
+ }),
+ // File nodes
+ setFile: (next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, this);
+ return result;
+ },
+ setFilePath: (next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, this);
+ return result;
+ }
+ });
+ this.runAfterInitialized(node, () => {
+ this.plugin.app.workspace.trigger("advanced-canvas:node-added", node.canvas, node);
+ this.plugin.app.workspace.trigger("advanced-canvas:node-changed", node.canvas, node);
+ });
+ }
+ patchEdge(edge) {
+ const that = this;
+ Patcher.patch(this.plugin, edge, {
+ setData: Patcher.OverrideExisting((next) => function(data, addHistory) {
+ const result = next.call(this, data);
+ if (this.initialized && !this.isDirty) {
+ this.isDirty = true;
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-changed", this.canvas, this);
+ delete this.isDirty;
+ }
+ this.canvas.data = this.canvas.getData();
+ if (this.initialized) this.canvas.view.requestSave();
+ if (addHistory) this.canvas.pushHistory(this.canvas.getData());
+ return result;
+ }),
+ render: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-changed", this.canvas, this);
+ return result;
+ }),
+ getCenter: Patcher.OverrideExisting((next) => function(...args) {
+ const result = next.call(this, ...args);
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-center-requested", this.canvas, this, result);
+ return result;
+ }),
+ onConnectionPointerdown: Patcher.OverrideExisting((next) => function(e) {
+ const cancelRef = { value: false };
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-try-dragging:before", this.canvas, this, e, cancelRef);
+ if (cancelRef.value) return;
+ const previousEnds = { from: this.from, to: this.to };
+ const result = next.call(this, e);
+ const eventPos = this.canvas.posFromEvt(e);
+ const fromPos = BBoxHelper.getCenterOfBBoxSide(this.from.node.getBBox(), this.from.side);
+ const toPos = BBoxHelper.getCenterOfBBoxSide(this.to.node.getBBox(), this.to.side);
+ const draggingSide = Math.hypot(eventPos.x - fromPos.x, eventPos.y - fromPos.y) > Math.hypot(eventPos.x - toPos.x, eventPos.y - toPos.y) ? "to" : "from";
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:before", this.canvas, this, e, false, draggingSide, previousEnds);
+ document.addEventListener("pointerup", (e2) => {
+ that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:after", this.canvas, this, e2, false, draggingSide, previousEnds);
+ }, { once: true });
+ return result;
+ })
+ });
+ this.runAfterInitialized(edge, () => {
+ this.plugin.app.workspace.trigger("advanced-canvas:edge-added", edge.canvas, edge);
+ });
+ }
+ runAfterInitialized(canvasElement, onReady) {
+ if (canvasElement.initialized) {
+ onReady();
+ return;
+ }
+ const that = this;
+ const uninstall = around(canvasElement, {
+ initialize: (next) => function(...args) {
+ const result = next.call(this, ...args);
+ onReady();
+ uninstall();
+ return result;
+ }
+ });
+ that.plugin.register(uninstall);
+ }
+};
+
+// src/patchers/link-suggestions-patcher.ts
+var import_obsidian6 = require("obsidian");
+var LinkSuggestionsPatcher = class extends Patcher {
+ async patch() {
+ var _a;
+ if (!this.plugin.settings.getSetting("enableSingleNodeLinks")) return;
+ const suggestManager = (_a = this.plugin.app.workspace.editorSuggest.suggests.find((s) => s.suggestManager)) == null ? void 0 : _a.suggestManager;
+ if (!suggestManager) return console.warn("LinkSuggestionsPatcher: No suggest manager found.");
+ Patcher.patchThisAndPrototype(this.plugin, suggestManager, {
+ getHeadingSuggestions: Patcher.OverrideExisting((next) => async function(context, path, subpath) {
+ const result = await next.call(this, context, path, subpath);
+ if (!path.endsWith(".canvas")) return result;
+ const currentFilePath = this.getSourcePath();
+ const targetFile = this.app.metadataCache.getFirstLinkpathDest(path, currentFilePath);
+ if (!targetFile) return result;
+ if (!(targetFile instanceof import_obsidian6.TFile) || targetFile.extension !== "canvas")
+ return result;
+ const fileCache = this.app.metadataCache.getFileCache(targetFile);
+ if (!fileCache) return result;
+ const canvasNodeCaches = fileCache.nodes;
+ if (!canvasNodeCaches) return result;
+ for (const nodeId of Object.keys(canvasNodeCaches)) {
+ if (nodeId === subpath) continue;
+ const suggestion = {
+ file: targetFile,
+ heading: nodeId,
+ level: 1,
+ matches: [],
+ path,
+ subpath: `#${nodeId}`,
+ score: 0,
+ type: "heading"
+ };
+ result.push(suggestion);
+ }
+ return result;
+ })
+ });
+ }
+};
+
+// src/advanced-canvas-embed.ts
+var import_obsidian7 = require("obsidian");
+var AdvancedCanvasEmbed = class extends import_obsidian7.Component {
+ constructor(context, file, subpath) {
+ super();
+ this.onModifyCallback = (file) => {
+ if (file.path !== this.file.path) return;
+ this.loadFile();
+ };
+ this.context = context;
+ this.file = file;
+ this.subpath = subpath;
+ if (!subpath) console.warn("AdvancedCanvasEmbed: No subpath provided. This embed will not work as expected.");
+ }
+ onload() {
+ this.context.app.vault.on("modify", this.onModifyCallback);
+ }
+ onunload() {
+ this.context.app.vault.off("modify", this.onModifyCallback);
+ }
+ async loadFile() {
+ if (!this.subpath) return;
+ const nodeId = this.subpath.replace(/^#/, "");
+ const canvasContent = await this.context.app.vault.cachedRead(this.file);
+ if (!canvasContent) return console.warn("AdvancedCanvasEmbed: No canvas content found.");
+ const canvasJson = JSON.parse(canvasContent);
+ const canvasNode = canvasJson.nodes.find((node) => node.id === nodeId);
+ if (!canvasNode) {
+ this.context.containerEl.classList.add("mod-empty");
+ this.context.containerEl.textContent = "Node not found";
+ return;
+ }
+ let nodeContent = "";
+ if (canvasNode.type === "text") nodeContent = canvasNode.text;
+ else if (canvasNode.type === "group") nodeContent = `**Group Node:** ${canvasNode.label}`;
+ else if (canvasNode.type === "file") nodeContent = `**File Node:** ${canvasNode.file}`;
+ this.context.containerEl.classList.add("markdown-embed");
+ this.context.containerEl.empty();
+ import_obsidian7.MarkdownRenderer.render(this.context.app, nodeContent, this.context.containerEl, this.file.path, this);
+ }
+};
+
+// src/patchers/embed-patcher.ts
+var EmbedPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("enableSingleNodeLinks")) return;
+ Patcher.patch(this.plugin, this.plugin.app.embedRegistry.embedByExtension, {
+ canvas: (next) => function(context, file, subpath) {
+ if (subpath) return new AdvancedCanvasEmbed(context, file, subpath);
+ return next.call(this, context, file, subpath);
+ }
+ });
+ }
+};
+
+// src/patchers/metadata-cache-patcher.ts
+var import_obsidian8 = require("obsidian");
+
+// src/utils/filepath-helper.ts
+var FilepathHelper = class {
+ static extension(path) {
+ var _a;
+ return (path == null ? void 0 : path.includes(".")) ? (_a = path == null ? void 0 : path.split(".")) == null ? void 0 : _a.pop() : void 0;
+ }
+};
+
+// src/utils/hash-helper.ts
+var HashHelper = class _HashHelper {
+ static async getBytesHash(bytes) {
+ const cryptoBytes = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
+ return _HashHelper.arrayBufferToHexString(cryptoBytes);
+ }
+ static arrayBufferToHexString(buffer) {
+ const uint8Array = new Uint8Array(buffer);
+ const hexArray = [];
+ for (const byte of uint8Array) {
+ hexArray.push((byte >>> 4).toString(16));
+ hexArray.push((byte & 15).toString(16));
+ }
+ return hexArray.join("");
+ }
+};
+
+// src/utils/task-queue.ts
+var TaskQueue = class {
+ constructor() {
+ this.running = false;
+ this.queue = [];
+ this.onFinished = () => {
+ };
+ }
+ async add(task) {
+ return new Promise((resolve) => {
+ this.queue.push([resolve, task]);
+ if (!this.running) this.run();
+ });
+ }
+ setOnFinished(callback) {
+ this.onFinished = callback;
+ }
+ async run() {
+ var _a;
+ this.running = true;
+ while (this.queue.length > 0) {
+ const [resolver, task] = (_a = this.queue.shift()) != null ? _a : [void 0, void 0];
+ await (task == null ? void 0 : task());
+ resolver == null ? void 0 : resolver();
+ }
+ this.running = false;
+ this.onFinished();
+ }
+};
+
+// src/patchers/metadata-cache-patcher.ts
+var MetadataCachePatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ Patcher.patchPrototype(this.plugin, this.plugin.app.metadataCache, {
+ getCache: Patcher.OverrideExisting((next) => function(filepath, ...args) {
+ if (FilepathHelper.extension(filepath) === "canvas") {
+ if (!this.fileCache.hasOwnProperty(filepath)) return null;
+ const hash = this.fileCache[filepath].hash;
+ return this.metadataCache[hash] || null;
+ }
+ return next.call(this, filepath, ...args);
+ }),
+ computeFileMetadataAsync: Patcher.OverrideExisting((next) => async function(file, ...args) {
+ if (file instanceof import_obsidian8.TFile && (file == null ? void 0 : file.extension) === "canvas")
+ return CanvasMetadataHandler.computeCanvasFileMetadataAsync.call(this, file);
+ return next.call(this, file, ...args);
+ }),
+ resolveLinks: Patcher.OverrideExisting((next) => async function(filepath) {
+ const result = next.call(this, filepath);
+ if (FilepathHelper.extension(filepath) === "canvas")
+ CanvasMetadataHandler.resolveCanvasLinks.call(this, filepath);
+ return result;
+ })
+ });
+ }
+};
+var _CanvasMetadataHandler = class _CanvasMetadataHandler {
+ static async computeCanvasFileMetadataAsync(file) {
+ this.uniqueFileLookup.add(file.name.toLowerCase(), file);
+ let isStale = true;
+ if (!this.fileCache.hasOwnProperty(file.path))
+ this.saveFileCache(file.path, { mtime: 0, size: 0, hash: "" });
+ else {
+ const cache2 = this.fileCache[file.path];
+ const unchanged = cache2.mtime === file.stat.mtime && cache2.size === file.stat.size;
+ const hasMetadataCache = cache2.hash && this.metadataCache.hasOwnProperty(cache2.hash);
+ if (unchanged && hasMetadataCache)
+ isStale = false;
+ }
+ if (isStale) {
+ _CanvasMetadataHandler.linkResolveQueue.setOnFinished(() => this.trigger("finished"));
+ await _CanvasMetadataHandler.metadataQueue.add(
+ () => _CanvasMetadataHandler.updateMetadataCache.call(this, file)
+ );
+ }
+ _CanvasMetadataHandler.linkResolveQueue.setOnFinished(() => this.trigger("resolved"));
+ await _CanvasMetadataHandler.linkResolveQueue.add(
+ () => _CanvasMetadataHandler.resolveCanvasLinks.call(this, file.path)
+ );
+ }
+ static async updateMetadataCache(file) {
+ const bytes = await this.vault.readBinary(file);
+ const data = new TextDecoder().decode(new Uint8Array(bytes));
+ const hash = await HashHelper.getBytesHash(bytes);
+ const cache2 = {
+ mtime: file.stat.mtime,
+ size: file.stat.size,
+ hash
+ };
+ this.saveFileCache(file.path, cache2);
+ let metadata = this.metadataCache[cache2.hash];
+ if (metadata) return this.trigger(
+ "changed",
+ file,
+ data,
+ metadata
+ );
+ const slowIndexingTimeout = setTimeout(() => {
+ new import_obsidian8.Notice(`Canvas indexing taking a long time for file ${file.path}`);
+ }, 1e4);
+ try {
+ metadata = await _CanvasMetadataHandler.computeCanvasMetadataAsync.call(this, data);
+ } finally {
+ clearTimeout(slowIndexingTimeout);
+ }
+ if (metadata) {
+ this.saveMetaCache(hash, metadata);
+ this.trigger("changed", file, data, metadata);
+ } else {
+ console.log("Canvas metadata failed to parse", file);
+ }
+ }
+ static async computeCanvasMetadataAsync(data) {
+ var _a, _b, _c, _d, _e;
+ const content = JSON.parse(data || "{}");
+ const metadata = {
+ v: 1
+ };
+ const frontmatter = (_a = content.metadata) == null ? void 0 : _a.frontmatter;
+ metadata.frontmatterPosition = {
+ start: { line: 0, col: 0, offset: 0 },
+ end: { line: 0, col: 0, offset: 0 }
+ };
+ metadata.frontmatter = frontmatter;
+ metadata.frontmatterLinks = [];
+ for (const [key, value] of Object.entries(frontmatter != null ? frontmatter : {})) {
+ const getLinks = (value2) => value2.map((v) => {
+ if (!v.startsWith("[[") || !v.endsWith("]]")) return null;
+ const [link, ...aliases] = v.slice(2, -2).split("|");
+ return {
+ key,
+ displayText: aliases.length > 0 ? aliases.join("|") : link,
+ link,
+ original: v
+ };
+ }).filter((v) => v !== null);
+ if (typeof value === "string") (_b = metadata.frontmatterLinks) == null ? void 0 : _b.push(...getLinks([value]));
+ else if (Array.isArray(value)) (_c = metadata.frontmatterLinks) == null ? void 0 : _c.push(...getLinks(value));
+ }
+ metadata.nodes = {};
+ metadata.links = [];
+ metadata.embeds = [];
+ await Promise.all(((_d = content.nodes) != null ? _d : []).map(async (node, index) => {
+ var _a2, _b2;
+ if (node.type !== "text") return;
+ const text = node.text;
+ const buffer = new TextEncoder().encode(text).buffer;
+ const nodeMetadata = await this.computeMetadataAsync(buffer);
+ if (!nodeMetadata) return;
+ metadata.nodes[node.id] = nodeMetadata;
+ metadata.links.push(...((_a2 = nodeMetadata.links) != null ? _a2 : []).map((link) => ({
+ ...link,
+ position: {
+ nodeId: node.id,
+ start: { line: 0, col: 1, offset: 0 },
+ // 0 for node
+ end: { line: 0, col: 1, offset: index }
+ // index of node
+ }
+ })));
+ metadata.embeds.push(...((_b2 = nodeMetadata.embeds) != null ? _b2 : []).map((embed2) => ({
+ ...embed2,
+ position: {
+ nodeId: node.id,
+ start: { line: 0, col: 1, offset: 0 },
+ // 0 for node
+ end: { line: 0, col: 1, offset: index }
+ // index of node
+ }
+ })));
+ }));
+ for (const [index, node] of ((_e = content.nodes) != null ? _e : []).entries()) {
+ if (node.type !== "file") continue;
+ const file = node.file;
+ if (!file) continue;
+ metadata.embeds.push({
+ link: file,
+ original: file,
+ displayText: file,
+ position: {
+ start: { line: 0, col: 1, offset: 0 },
+ // 0 for nodes
+ end: { line: 0, col: 1, offset: index }
+ // index of node
+ }
+ });
+ }
+ return metadata;
+ }
+ static async resolveCanvasLinks(filepath) {
+ var _a, _b, _c, _d, _e;
+ const file = this.vault.getAbstractFileByPath(filepath);
+ if (!(file instanceof import_obsidian8.TFile)) return;
+ const metadata = this.getFileCache(file);
+ const references = [...(_a = metadata == null ? void 0 : metadata.links) != null ? _a : [], ...(_b = metadata == null ? void 0 : metadata.embeds) != null ? _b : []];
+ const referenceLinks = references.map((ref) => ref.link).sort();
+ const resolvedLinks = {};
+ const unresolvedLinks = {};
+ for (const link of referenceLinks) {
+ const resolved = this.getFirstLinkpathDest(link, filepath);
+ if (resolved) {
+ (_d = resolvedLinks[_c = resolved.path]) != null ? _d : resolvedLinks[_c] = 0;
+ resolvedLinks[resolved.path]++;
+ } else {
+ const strippedLink = link.endsWith(".md") ? link.slice(0, -3) : link;
+ (_e = unresolvedLinks[strippedLink]) != null ? _e : unresolvedLinks[strippedLink] = 0;
+ unresolvedLinks[strippedLink]++;
+ }
+ }
+ this.resolvedLinks[filepath] = resolvedLinks;
+ this.unresolvedLinks[filepath] = unresolvedLinks;
+ await sleep(1);
+ this.trigger("resolve", file);
+ }
+};
+_CanvasMetadataHandler.metadataQueue = new TaskQueue();
+_CanvasMetadataHandler.linkResolveQueue = new TaskQueue();
+var CanvasMetadataHandler = _CanvasMetadataHandler;
+
+// src/patchers/backlinks-patcher.ts
+var import_obsidian9 = require("obsidian");
+var BacklinksPatcher = class extends Patcher {
+ constructor() {
+ super(...arguments);
+ this.isRecomputingBacklinks = false;
+ }
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ const that = this;
+ await Patcher.waitForViewRequest(this.plugin, "backlink", (view) => {
+ Patcher.patchPrototype(this.plugin, view.backlink, {
+ recomputeBacklink: Patcher.OverrideExisting((next) => function(file, ...args) {
+ that.isRecomputingBacklinks = true;
+ const result = next.call(this, file, ...args);
+ that.isRecomputingBacklinks = false;
+ return result;
+ })
+ });
+ });
+ Patcher.patchPrototype(this.plugin, this.plugin.app.vault, {
+ recurseChildrenAC: (_next) => function(origin, traverse) {
+ for (let stack = [origin]; stack.length > 0; ) {
+ const current = stack.pop();
+ if (current) {
+ traverse(current);
+ if (current instanceof import_obsidian9.TFolder) stack = stack.concat(current.children);
+ }
+ }
+ },
+ getMarkdownFiles: Patcher.OverrideExisting((next) => function(...args) {
+ if (!that.isRecomputingBacklinks) return next.call(this, ...args);
+ const files = [];
+ const root = this.getRoot();
+ this.recurseChildrenAC(root, (child) => {
+ if (child instanceof import_obsidian9.TFile && (child.extension === "md" || child.extension === "canvas")) {
+ files.push(child);
+ }
+ });
+ return files;
+ })
+ });
+ }
+};
+
+// src/patchers/outgoing-links-patcher.ts
+var OutgoingLinksPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ await Patcher.waitForViewRequest(this.plugin, "outgoing-link", (view) => {
+ Patcher.patchPrototype(this.plugin, view.outgoingLink, {
+ recomputeLinks: Patcher.OverrideExisting((next) => function(...args) {
+ var _a;
+ const isCanvas = ((_a = this.file) == null ? void 0 : _a.extension) === "canvas";
+ if (isCanvas) this.file.extension = "md";
+ const result = next.call(this, ...args);
+ if (isCanvas) this.file.extension = "canvas";
+ return result;
+ }),
+ recomputeUnlinked: Patcher.OverrideExisting((next) => function(...args) {
+ var _a;
+ const isCanvas = ((_a = this.file) == null ? void 0 : _a.extension) === "canvas";
+ if (isCanvas) this.file.extension = "md";
+ const result = next.call(this, ...args);
+ if (isCanvas) this.file.extension = "canvas";
+ return result;
+ })
+ });
+ });
+ }
+};
+
+// src/patchers/file-manager-patcher.ts
+var FileManagerPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ const that = this;
+ Patcher.patch(this.plugin, this.plugin.app.fileManager, {
+ processFrontMatter: Patcher.OverrideExisting((next) => async function(file, fn, options) {
+ if ((file == null ? void 0 : file.extension) === "canvas") {
+ that.plugin.app.vault.process(file, (data) => {
+ const content = JSON.parse(data);
+ fn(content.metadata.frontmatter);
+ return JSON.stringify(content, null, 2);
+ });
+ return;
+ }
+ return next.call(this, file, fn, options);
+ })
+ });
+ }
+};
+
+// src/patchers/properties-patcher.ts
+var PropertiesPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ if (!this.plugin.app.viewRegistry.viewByType["file-properties"]) return;
+ await Patcher.waitForViewRequest(this.plugin, "file-properties", (view) => {
+ Patcher.patchPrototype(this.plugin, view, {
+ isSupportedFile: Patcher.OverrideExisting((next) => function(file) {
+ if ((file == null ? void 0 : file.extension) === "canvas") return true;
+ return next.call(this, file);
+ }),
+ updateFrontmatter: Patcher.OverrideExisting((next) => function(file, content) {
+ var _a, _b, _c;
+ if ((file == null ? void 0 : file.extension) === "canvas") {
+ let frontmatter;
+ try {
+ frontmatter = (_c = (_b = (_a = JSON.parse(content)) == null ? void 0 : _a.metadata) == null ? void 0 : _b.frontmatter) != null ? _c : {};
+ } catch (e) {
+ frontmatter = {};
+ }
+ this.rawFrontmatter = JSON.stringify(frontmatter, null, 2);
+ this.frontmatter = frontmatter;
+ return frontmatter;
+ }
+ return next.call(this, file, content);
+ }),
+ saveFrontmatter: Patcher.OverrideExisting((next) => function(frontmatter) {
+ var _a;
+ if (((_a = this.file) == null ? void 0 : _a.extension) === "canvas") {
+ if (this.file !== this.modifyingFile) return;
+ this.app.vault.process(this.file, (data) => {
+ const content = JSON.parse(data);
+ if (content == null ? void 0 : content.metadata) content.metadata.frontmatter = frontmatter;
+ return JSON.stringify(content, null, 2);
+ });
+ return;
+ }
+ return next.call(this, frontmatter);
+ })
+ });
+ });
+ }
+};
+
+// src/patchers/search-patcher.ts
+var SearchPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return;
+ const that = this;
+ await Patcher.waitForViewRequest(this.plugin, "search", (view) => {
+ const uninstallers = [];
+ Patcher.patchThisAndPrototype(this.plugin, view, {
+ startSearch: (next) => function(...args) {
+ const result = next.call(this, ...args);
+ if (this.searchQuery) {
+ that.patchSearchQuery(this.searchQuery);
+ uninstallers.forEach((uninstall) => uninstall());
+ }
+ return result;
+ }
+ }, uninstallers);
+ });
+ }
+ patchSearchQuery(searchQuery) {
+ Patcher.patchThisAndPrototype(this.plugin, searchQuery, {
+ _match: Patcher.OverrideExisting((next) => function(data) {
+ var _a, _b;
+ const isCanvas = (_b = (_a = data.strings.filepath) == null ? void 0 : _a.endsWith(".canvas")) != null ? _b : false;
+ if (isCanvas && !data.cache)
+ data.cache = this.app.metadataCache.getCache(data.strings.filepath);
+ return next.call(this, data);
+ })
+ });
+ }
+};
+
+// src/patchers/search-command-patcher.ts
+var import_obsidian10 = require("obsidian");
+var SearchCommandPatcher = class extends Patcher {
+ async patch() {
+ if (!this.plugin.settings.getSetting("nativeFileSearchEnabled")) return;
+ const that = this;
+ Patcher.patch(this.plugin, this.plugin.app.commands.commands["editor:open-search"], {
+ checkCallback: Patcher.OverrideExisting((next) => function(checking) {
+ if (that.plugin.app.workspace.activeEditor) return next.call(this, checking);
+ const activeCanvasView = that.plugin.getCurrentCanvasView();
+ if (!activeCanvasView) return next.call(this, checking);
+ if (checking) return true;
+ if (!activeCanvasView.canvas.searchEl) new CanvasSearchView(activeCanvasView);
+ return true;
+ })
+ });
+ }
+};
+var CanvasSearchView = class {
+ constructor(view) {
+ this.searchMatches = [];
+ this.matchIndex = 0;
+ this.view = view;
+ this.createSearchView();
+ }
+ createSearchView() {
+ this.containerEl = document.createElement("div");
+ this.containerEl.className = "document-search-container";
+ const documentSearch = document.createElement("div");
+ documentSearch.className = "document-search";
+ this.containerEl.appendChild(documentSearch);
+ const searchInputContainer = document.createElement("div");
+ searchInputContainer.className = "search-input-container document-search-input";
+ documentSearch.appendChild(searchInputContainer);
+ this.searchInput = document.createElement("input");
+ this.searchInput.type = "text";
+ this.searchInput.placeholder = "Find...";
+ this.searchInput.addEventListener("keydown", (e) => this.onKeyDown(e));
+ this.searchInput.addEventListener("input", () => this.onInput());
+ searchInputContainer.appendChild(this.searchInput);
+ this.searchCount = document.createElement("div");
+ this.searchCount.className = "document-search-count";
+ this.searchCount.style.display = "none";
+ this.searchCount.textContent = "0 / 0";
+ searchInputContainer.appendChild(this.searchCount);
+ const documentSearchButtons = document.createElement("div");
+ documentSearchButtons.className = "document-search-buttons";
+ documentSearch.appendChild(documentSearchButtons);
+ const previousButton = document.createElement("button");
+ previousButton.className = "clickable-icon document-search-button";
+ previousButton.setAttribute("aria-label", "Previous\nShift + F3");
+ previousButton.setAttribute("data-tooltip-position", "top");
+ (0, import_obsidian10.setIcon)(previousButton, "arrow-up");
+ previousButton.addEventListener("click", () => this.changeMatch(this.matchIndex - 1));
+ documentSearchButtons.appendChild(previousButton);
+ const nextButton = document.createElement("button");
+ nextButton.className = "clickable-icon document-search-button";
+ nextButton.setAttribute("aria-label", "Next\nF3");
+ nextButton.setAttribute("data-tooltip-position", "top");
+ (0, import_obsidian10.setIcon)(nextButton, "arrow-down");
+ nextButton.addEventListener("click", () => this.changeMatch(this.matchIndex + 1));
+ documentSearchButtons.appendChild(nextButton);
+ const closeButton = document.createElement("button");
+ closeButton.className = "clickable-icon document-search-close-button";
+ closeButton.setAttribute("aria-label", "Exit search");
+ closeButton.setAttribute("data-tooltip-position", "top");
+ (0, import_obsidian10.setIcon)(closeButton, "x");
+ closeButton.addEventListener("click", () => this.close());
+ documentSearch.appendChild(closeButton);
+ this.view.canvas.wrapperEl.appendChild(this.containerEl);
+ this.view.canvas.searchEl = this.containerEl;
+ this.searchInput.focus();
+ }
+ onKeyDown(e) {
+ if (e.key === "Enter" || e.key === "F3")
+ this.changeMatch(this.matchIndex + (e.shiftKey ? -1 : 1));
+ else if (e.key === "Escape")
+ this.close();
+ }
+ onInput() {
+ const hasQuery = this.searchInput.value.length > 0;
+ this.searchCount.style.display = hasQuery ? "block" : "none";
+ if (!hasQuery) this.searchMatches = [];
+ else {
+ this.searchMatches = Array.from(this.view.canvas.nodes.values()).map((node) => {
+ const nodeData = node.getData();
+ let content = void 0;
+ if (nodeData.type === "text") content = nodeData.text;
+ else if (nodeData.type === "group") content = nodeData.label;
+ else if (nodeData.type === "file") content = node.child.data;
+ if (!content) return null;
+ const matches = [];
+ const regex = new RegExp(this.searchInput.value, "gi");
+ let match;
+ while ((match = regex.exec(content)) !== null) {
+ matches.push([match.index, match.index + match[0].length]);
+ }
+ return { nodeId: node.id, content, matches };
+ }).filter((match) => match && match.matches.length > 0);
+ }
+ this.changeMatch(0);
+ }
+ changeMatch(index) {
+ if (this.searchMatches.length === 0) this.matchIndex = -1;
+ else {
+ if (index < 0) index += this.searchMatches.length;
+ this.matchIndex = index % this.searchMatches.length;
+ }
+ const match = this.searchMatches[this.matchIndex];
+ if (match) this.goToMatch(match);
+ this.searchCount.textContent = `${this.matchIndex + 1} / ${this.searchMatches.length}`;
+ }
+ goToMatch(match) {
+ this.view.setEphemeralState({ match });
+ }
+ close() {
+ this.containerEl.remove();
+ this.view.canvas.searchEl = void 0;
+ }
+};
+
+// src/canvas-extensions/metadata-canvas-extension.ts
+var import_obsidian11 = require("obsidian");
+var MetadataCanvasExtension = class extends CanvasExtension {
+ constructor() {
+ super(...arguments);
+ this.canvasCssclassesCache = /* @__PURE__ */ new Map();
+ }
+ isEnabled() {
+ return true;
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.onCanvasChanged(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-metadata-changed",
+ (canvas) => this.onMetadataChanged(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-view-unloaded:before",
+ (view) => this.onCanvasViewUnloaded(view)
+ ));
+ }
+ onCanvasChanged(canvas) {
+ var _a;
+ const metadata = (_a = canvas.data) == null ? void 0 : _a.metadata;
+ if (!metadata || metadata.version !== CURRENT_SPEC_VERSION)
+ return new import_obsidian11.Notice("Metadata node not found or version mismatch. Should have been migrated (but wasn't).");
+ const that = this;
+ const validator = {
+ get(target, key) {
+ if (typeof target[key] === "object" && target[key] !== null)
+ return new Proxy(target[key], validator);
+ else return target[key];
+ },
+ set(target, key, value) {
+ target[key] = value;
+ that.plugin.app.workspace.trigger("advanced-canvas:canvas-metadata-changed", canvas);
+ canvas.requestSave();
+ return true;
+ }
+ };
+ canvas.metadata = new Proxy(metadata, validator);
+ this.plugin.app.workspace.trigger("advanced-canvas:canvas-metadata-changed", canvas);
+ }
+ onMetadataChanged(canvas) {
+ var _a, _b, _c;
+ const oldCssClasses = this.canvasCssclassesCache.get(canvas.view);
+ if (oldCssClasses) canvas.wrapperEl.classList.remove(...oldCssClasses);
+ const currentClasses = (_c = (_b = (_a = canvas.metadata) == null ? void 0 : _a.frontmatter) == null ? void 0 : _b.cssclasses) != null ? _c : [];
+ this.canvasCssclassesCache.set(canvas.view, currentClasses);
+ if (currentClasses.length > 0) canvas.wrapperEl.classList.add(...currentClasses);
+ }
+ onCanvasViewUnloaded(view) {
+ this.canvasCssclassesCache.delete(view);
+ }
+};
+
+// src/utils/modal-helper.ts
+var import_obsidian12 = require("obsidian");
+var AbstractSelectionModal = class extends import_obsidian12.FuzzySuggestModal {
+ constructor(app, placeholder, suggestions) {
+ super(app);
+ this.suggestions = suggestions;
+ this.setPlaceholder(placeholder);
+ this.setInstructions([{
+ command: "\u2191\u2193",
+ purpose: "to navigate"
+ }, {
+ command: "esc",
+ purpose: "to dismiss"
+ }]);
+ }
+ getItems() {
+ return this.suggestions;
+ }
+ getItemText(item) {
+ return item;
+ }
+ onChooseItem(item, evt) {
+ }
+ awaitInput() {
+ return new Promise((resolve, _reject) => {
+ this.onChooseItem = (item) => {
+ resolve(item);
+ };
+ this.open();
+ });
+ }
+};
+var FileNameModal = class extends import_obsidian12.SuggestModal {
+ constructor(app, parentPath, fileExtension) {
+ super(app);
+ this.parentPath = parentPath.replace(/^\//, "").replace(/\/$/, "");
+ this.fileExtension = fileExtension;
+ }
+ getSuggestions(query) {
+ const queryWithoutExtension = query.replace(new RegExp(`\\.${this.fileExtension}$`), "");
+ if (queryWithoutExtension === "") return [];
+ const queryWithExtension = queryWithoutExtension + "." + this.fileExtension;
+ const suggestions = [queryWithExtension];
+ if (this.parentPath.length > 0) suggestions.splice(0, 0, `${this.parentPath}/${queryWithExtension}`);
+ return suggestions.filter((s) => this.app.vault.getAbstractFileByPath(s) === null);
+ }
+ renderSuggestion(text, el) {
+ el.setText(text);
+ }
+ onChooseSuggestion(_text, _evt) {
+ }
+ awaitInput() {
+ return new Promise((resolve, _reject) => {
+ this.onChooseSuggestion = (text) => {
+ resolve(text);
+ };
+ this.open();
+ });
+ }
+};
+var FileSelectModal = class extends import_obsidian12.SuggestModal {
+ constructor(app, extensionsRegex, suggestNewFile = false) {
+ super(app);
+ this.files = this.app.vault.getFiles().map((file) => file.path).filter((path) => {
+ var _a;
+ return (_a = FilepathHelper.extension(path)) == null ? void 0 : _a.match(extensionsRegex != null ? extensionsRegex : /.*/);
+ });
+ this.suggestNewFile = suggestNewFile;
+ this.setPlaceholder("Type to search...");
+ this.setInstructions([{
+ command: "\u2191\u2193",
+ purpose: "to navigate"
+ }, {
+ command: "\u21B5",
+ purpose: "to open"
+ }, {
+ command: "shift \u21B5",
+ purpose: "to create"
+ }, {
+ command: "esc",
+ purpose: "to dismiss"
+ }]);
+ this.scope.register(["Shift"], "Enter", (e) => {
+ this.onChooseSuggestion(this.inputEl.value, e);
+ this.close();
+ });
+ }
+ getSuggestions(query) {
+ const suggestions = this.files.filter((path) => path.toLowerCase().includes(query.toLowerCase()));
+ if (suggestions.length === 0 && this.suggestNewFile) suggestions.push(query);
+ return suggestions;
+ }
+ renderSuggestion(path, el) {
+ const simplifiedPath = path.replace(/\.md$/, "");
+ el.setText(simplifiedPath);
+ }
+ onChooseSuggestion(_path, _evt) {
+ }
+ awaitInput() {
+ return new Promise((resolve, _reject) => {
+ this.onChooseSuggestion = (path, _evt) => {
+ const file = this.app.vault.getAbstractFileByPath(path);
+ if (file instanceof import_obsidian12.TFile)
+ return resolve(file);
+ if (!this.suggestNewFile) return;
+ if (FilepathHelper.extension(path) === void 0) path += ".md";
+ const newFile = this.app.vault.create(path, "");
+ resolve(newFile);
+ };
+ this.open();
+ });
+ }
+};
+
+// src/canvas-extensions/node-ratio-canvas-extension.ts
+var NodeRatioCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return true;
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "canvas:node-menu",
+ (menu, node) => this.onNodeMenu(menu, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-resized",
+ (canvas, node) => this.onNodeResized(canvas, node)
+ ));
+ }
+ onNodeMenu(menu, node) {
+ if (!this.plugin.settings.getSetting("aspectRatioControlFeatureEnabled")) return;
+ menu.addItem((item) => {
+ item.setTitle("Set Aspect Ratio").setIcon("aspect-ratio").onClick(async () => {
+ const NO_RATIO = "No ratio enforcement";
+ const newRatioString = await new AbstractSelectionModal(this.plugin.app, "Enter aspect ratio (width:height)", ["16:9", "4:3", "3:2", "1:1", NO_RATIO]).awaitInput();
+ const nodeData = node.getData();
+ if (newRatioString === NO_RATIO) {
+ node.setData({
+ ...nodeData,
+ ratio: void 0
+ });
+ return;
+ }
+ const [width, height] = newRatioString.split(":").map(Number);
+ if (width && height) {
+ node.setData({
+ ...nodeData,
+ ratio: width / height
+ });
+ node.setData({
+ ...node.getData(),
+ width: nodeData.height * (width / height)
+ });
+ }
+ });
+ });
+ }
+ onNodeResized(_canvas, node) {
+ const nodeData = node.getData();
+ if (!nodeData.ratio) return;
+ const nodeBBox = node.getBBox();
+ const nodeSize = {
+ width: nodeBBox.maxX - nodeBBox.minX,
+ height: nodeBBox.maxY - nodeBBox.minY
+ };
+ const nodeAspectRatio = nodeSize.width / nodeSize.height;
+ if (nodeAspectRatio < nodeData.ratio)
+ nodeSize.width = nodeSize.height * nodeData.ratio;
+ else nodeSize.height = nodeSize.width / nodeData.ratio;
+ node.setData({
+ ...nodeData,
+ width: nodeSize.width,
+ height: nodeSize.height
+ });
+ }
+};
+
+// src/canvas-extensions/group-canvas-extension.ts
+var GROUP_NODE_SIZE = { width: 300, height: 300 };
+var GROUP_NODE_PADDING = 20;
+var GroupCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return true;
+ }
+ init() {
+ this.plugin.addCommand({
+ id: "create-group-around-selection",
+ name: "Group selected nodes",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => this.createGroupAroundSelection(canvas)
+ )
+ });
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => {
+ CanvasHelper.addCardMenuOption(
+ canvas,
+ CanvasHelper.createCardMenuOption(
+ canvas,
+ {
+ id: "create-group",
+ label: "Drag to add group",
+ icon: "group"
+ },
+ () => GROUP_NODE_SIZE,
+ (canvas2, pos) => {
+ canvas2.createGroupNode({
+ pos,
+ size: GROUP_NODE_SIZE
+ });
+ }
+ )
+ );
+ }
+ ));
+ }
+ createGroupAroundSelection(canvas) {
+ const bbox = BBoxHelper.combineBBoxes(
+ Array.from(canvas.selection.values()).map((e) => e.getBBox())
+ );
+ const paddedBBox = BBoxHelper.enlargeBBox(bbox, GROUP_NODE_PADDING);
+ canvas.createGroupNode({
+ pos: { x: paddedBBox.minX, y: paddedBBox.minY },
+ size: {
+ width: paddedBBox.maxX - paddedBBox.minX,
+ height: paddedBBox.maxY - paddedBBox.minY
+ }
+ });
+ }
+};
+
+// src/canvas-extensions/presentation-canvas-extension.ts
+var import_obsidian13 = require("obsidian");
+var START_SLIDE_NAME = "Start Slide";
+var DEFAULT_SLIDE_NAME = "New Slide";
+var PresentationCanvasExtension = class extends CanvasExtension {
+ constructor() {
+ super(...arguments);
+ this.savedViewport = null;
+ this.isPresentationMode = false;
+ this.visitedNodeIds = [];
+ this.fullscreenModalObserver = null;
+ this.presentationUsesFullscreen = false;
+ }
+ isEnabled() {
+ return "presentationFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "canvas:selection-menu",
+ (menu, canvas) => {
+ menu.addItem(
+ (item) => item.setTitle("Wrap in slide").setIcon("gallery-vertical").onClick(() => this.addSlide(
+ canvas,
+ void 0,
+ BBoxHelper.enlargeBBox(BBoxHelper.combineBBoxes(
+ [...canvas.selection.values()].map((element) => element.getBBox())
+ ), this.plugin.settings.getSetting("wrapInSlidePadding"))
+ ))
+ );
+ }
+ ));
+ this.plugin.addCommand({
+ id: "create-new-slide",
+ name: "Create new slide",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && !this.isPresentationMode,
+ (canvas) => this.addSlide(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "set-start-node",
+ name: "Set start node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && !this.isPresentationMode && canvas.getSelectionData().nodes.length === 1,
+ (canvas) => this.setStartNode(canvas, canvas.nodes.get(canvas.getSelectionData().nodes[0].id))
+ )
+ });
+ this.plugin.addCommand({
+ id: "start-presentation",
+ name: "Start presentation",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => !this.isPresentationMode,
+ (canvas) => this.startPresentation(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "continue-presentation",
+ name: "Continue presentation",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => !this.isPresentationMode,
+ (canvas) => this.startPresentation(canvas, true)
+ )
+ });
+ this.plugin.addCommand({
+ id: "end-presentation",
+ name: "End presentation",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => this.isPresentationMode,
+ (canvas) => this.endPresentation(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "previous-node",
+ name: "Previous node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => this.isPresentationMode,
+ (canvas) => this.previousNode(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "next-node",
+ name: "Next node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => this.isPresentationMode,
+ (canvas) => this.nextNode(canvas)
+ )
+ });
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.onCanvasChanged(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ }
+ onCanvasChanged(canvas) {
+ CanvasHelper.addCardMenuOption(
+ canvas,
+ CanvasHelper.createCardMenuOption(
+ canvas,
+ {
+ id: "new-slide",
+ label: "Drag to add slide",
+ icon: "gallery-vertical"
+ },
+ () => this.getDefaultSlideSize(),
+ (canvas2, pos) => this.addSlide(canvas2, pos)
+ )
+ );
+ }
+ onPopupMenuCreated(canvas) {
+ if (!this.plugin.settings.getSetting("showSetStartNodeInPopup")) return;
+ const selectedNodesData = canvas.getSelectionData().nodes;
+ if (canvas.readonly || selectedNodesData.length !== 1 || canvas.selection.size > 1) return;
+ const selectedNode = canvas.nodes.get(selectedNodesData[0].id);
+ if (!selectedNode) return;
+ CanvasHelper.addPopupMenuOption(
+ canvas,
+ CanvasHelper.createPopupMenuOption({
+ id: "start-node",
+ label: "Set as start slide",
+ icon: "play",
+ callback: () => this.setStartNode(canvas, selectedNode)
+ })
+ );
+ }
+ setStartNode(canvas, node) {
+ if (!node) return;
+ canvas.metadata["startNode"] = node.getData().id;
+ }
+ getDefaultSlideSize() {
+ const slideSize = this.plugin.settings.getSetting("defaultSlideDimensions");
+ return { width: slideSize[0], height: slideSize[1] };
+ }
+ getSlideAspectRatio() {
+ const slideSize = this.getDefaultSlideSize();
+ return slideSize.width / slideSize.height;
+ }
+ addSlide(canvas, pos, bbox) {
+ const isStartNode = canvas.metadata["startNode"] === void 0;
+ const slideSize = this.getDefaultSlideSize();
+ const slideAspectRatio = this.getSlideAspectRatio();
+ if (bbox) {
+ const bboxWidth = bbox.maxX - bbox.minX;
+ const bboxHeight = bbox.maxY - bbox.minY;
+ if (bboxWidth / bboxHeight > slideAspectRatio) {
+ slideSize.width = bboxWidth;
+ slideSize.height = bboxWidth / slideAspectRatio;
+ } else {
+ slideSize.height = bboxHeight;
+ slideSize.width = bboxHeight * slideAspectRatio;
+ }
+ pos = {
+ x: bbox.minX,
+ y: bbox.minY
+ };
+ }
+ if (!pos) pos = CanvasHelper.getCenterCoordinates(canvas, this.getDefaultSlideSize());
+ const groupNode = canvas.createGroupNode({
+ pos,
+ size: slideSize,
+ label: isStartNode ? START_SLIDE_NAME : DEFAULT_SLIDE_NAME,
+ focus: false
+ });
+ groupNode.setData({
+ ...groupNode.getData(),
+ ratio: slideAspectRatio
+ });
+ if (isStartNode) canvas.metadata["startNode"] = groupNode.getData().id;
+ }
+ async animateNodeTransition(canvas, fromNode, toNode) {
+ const removePadding = this.plugin.settings.getSetting("zoomToSlideWithoutPadding");
+ const animationDurationMs = this.plugin.settings.getSetting("slideTransitionAnimationDuration") * 1e3;
+ const toNodeBBox = CanvasHelper.getSmallestAllowedZoomBBox(canvas, toNode.getBBox());
+ const toNodeBBoxPadded = removePadding ? toNodeBBox : BBoxHelper.enlargeBBox(toNodeBBox, 50);
+ console.log({ toNodeBBox, toNodeBBoxPadded });
+ if (animationDurationMs > 0 && fromNode) {
+ const animationIntensity = this.plugin.settings.getSetting("slideTransitionAnimationIntensity");
+ const fromNodeBBox = CanvasHelper.getSmallestAllowedZoomBBox(canvas, fromNode.getBBox());
+ const fromNodeBBoxPadded = removePadding ? fromNodeBBox : BBoxHelper.enlargeBBox(fromNodeBBox, 50);
+ const currentNodeBBoxEnlarged = BBoxHelper.scaleBBox(fromNodeBBoxPadded, animationIntensity);
+ canvas.zoomToRealBbox(currentNodeBBoxEnlarged);
+ await sleep(animationDurationMs / 2);
+ if (fromNode.getData().id !== toNode.getData().id) {
+ const nextNodeBBoxEnlarged = BBoxHelper.scaleBBox(toNodeBBoxPadded, animationIntensity);
+ canvas.zoomToRealBbox(nextNodeBBoxEnlarged);
+ await sleep(animationDurationMs / 2);
+ }
+ }
+ canvas.zoomToRealBbox(toNodeBBoxPadded);
+ }
+ async startPresentation(canvas, tryContinue = false) {
+ if (!tryContinue || this.visitedNodeIds.length === 0) {
+ const startNode2 = canvas.metadata["startNode"] && canvas.nodes.get(canvas.metadata["startNode"]);
+ if (!startNode2) {
+ new import_obsidian13.Notice("No start node found. Please mark a node as a start node trough the popup menu.");
+ return;
+ }
+ this.visitedNodeIds = [startNode2.getData().id];
+ }
+ this.savedViewport = {
+ x: canvas.tx,
+ y: canvas.ty,
+ zoom: canvas.tZoom
+ };
+ const shouldEnterFullscreen = this.plugin.settings.getSetting("fullscreenPresentationEnabled");
+ this.presentationUsesFullscreen = shouldEnterFullscreen;
+ canvas.wrapperEl.focus();
+ canvas.wrapperEl.classList.add("presentation-mode");
+ if (shouldEnterFullscreen) {
+ try {
+ await canvas.wrapperEl.requestFullscreen();
+ } catch (e) {
+ this.presentationUsesFullscreen = false;
+ }
+ }
+ canvas.setReadonly(true);
+ if (this.plugin.settings.getSetting("useUnclampedZoomWhilePresenting"))
+ canvas.screenshotting = true;
+ canvas.wrapperEl.onkeydown = (e) => {
+ if (e.key === "Escape") {
+ e.preventDefault();
+ e.stopPropagation();
+ this.endPresentation(canvas);
+ return;
+ }
+ if (this.plugin.settings.getSetting("useArrowKeysToChangeSlides")) {
+ if (e.key === "ArrowRight") this.nextNode(canvas);
+ else if (e.key === "ArrowLeft") this.previousNode(canvas);
+ }
+ if (this.plugin.settings.getSetting("usePgUpPgDownKeysToChangeSlides")) {
+ if (e.key === "PageDown") this.nextNode(canvas);
+ else if (e.key === "PageUp") this.previousNode(canvas);
+ }
+ };
+ if (this.presentationUsesFullscreen) {
+ this.fullscreenModalObserver = new MutationObserver((mutationRecords) => {
+ mutationRecords.forEach((mutationRecord) => {
+ mutationRecord.addedNodes.forEach((node) => {
+ var _a;
+ document.body.removeChild(node);
+ (_a = document.fullscreenElement) == null ? void 0 : _a.appendChild(node);
+ });
+ });
+ const inputField = document.querySelector(".prompt-input");
+ if (inputField) inputField.focus();
+ });
+ this.fullscreenModalObserver.observe(document.body, { childList: true });
+ canvas.wrapperEl.onfullscreenchange = (_e) => {
+ if (document.fullscreenElement) return;
+ this.endPresentation(canvas);
+ };
+ }
+ this.isPresentationMode = true;
+ if (this.presentationUsesFullscreen)
+ await sleep(500);
+ const startNodeId = this.visitedNodeIds.first();
+ if (!startNodeId) return;
+ const startNode = canvas.nodes.get(startNodeId);
+ if (!startNode) return;
+ this.animateNodeTransition(canvas, void 0, startNode);
+ }
+ endPresentation(canvas) {
+ var _a;
+ if (!this.isPresentationMode) return;
+ if (this.presentationUsesFullscreen) {
+ (_a = this.fullscreenModalObserver) == null ? void 0 : _a.disconnect();
+ this.fullscreenModalObserver = null;
+ canvas.wrapperEl.onfullscreenchange = null;
+ if (document.fullscreenElement) document.exitFullscreen();
+ }
+ canvas.wrapperEl.onkeydown = null;
+ canvas.setReadonly(false);
+ if (this.plugin.settings.getSetting("useUnclampedZoomWhilePresenting"))
+ canvas.screenshotting = false;
+ canvas.wrapperEl.classList.remove("presentation-mode");
+ if (this.plugin.settings.getSetting("resetViewportOnPresentationEnd"))
+ canvas.setViewport(this.savedViewport.x, this.savedViewport.y, this.savedViewport.zoom);
+ this.isPresentationMode = false;
+ this.presentationUsesFullscreen = false;
+ }
+ nextNode(canvas) {
+ var _a;
+ const fromNodeId = this.visitedNodeIds.last();
+ if (!fromNodeId) return;
+ const fromNode = canvas.nodes.get(fromNodeId);
+ if (!fromNode) return;
+ const outgoingEdges = canvas.getEdgesForNode(fromNode).filter((edge) => edge.from.node.getData().id === fromNodeId);
+ let toNode = (_a = outgoingEdges.first()) == null ? void 0 : _a.to.node;
+ if (outgoingEdges.length > 1) {
+ const sortedEdges = outgoingEdges.sort((a, b) => {
+ if (!a.label) return 1;
+ if (!b.label) return -1;
+ return a.label.localeCompare(b.label);
+ });
+ const traversedEdgesCount = this.visitedNodeIds.filter((visitedNodeId) => visitedNodeId === fromNodeId).length - 1;
+ const nextEdge = sortedEdges[traversedEdgesCount];
+ toNode = nextEdge.to.node;
+ }
+ if (toNode) {
+ this.visitedNodeIds.push(toNode.getData().id);
+ this.animateNodeTransition(canvas, fromNode, toNode);
+ } else {
+ this.animateNodeTransition(canvas, fromNode, fromNode);
+ }
+ }
+ previousNode(canvas) {
+ const fromNodeId = this.visitedNodeIds.pop();
+ if (!fromNodeId) return;
+ const fromNode = canvas.nodes.get(fromNodeId);
+ if (!fromNode) return;
+ const toNodeId = this.visitedNodeIds.last();
+ let toNode = toNodeId ? canvas.nodes.get(toNodeId) : null;
+ if (!toNode) {
+ toNode = fromNode;
+ this.visitedNodeIds.push(fromNodeId);
+ }
+ this.animateNodeTransition(canvas, fromNode, toNode);
+ }
+};
+
+// src/canvas-extensions/z-ordering-canvas-extension.ts
+var ZOrderingCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "zOrderingControlFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "canvas:node-menu",
+ (menu, node) => this.nodeContextMenu(node, menu)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "canvas:selection-menu",
+ (menu, canvas) => this.selectionContextMenu(canvas, menu)
+ ));
+ }
+ nodeContextMenu(node, menu) {
+ this.addZOrderingContextMenuItems(node.canvas, [node], menu);
+ }
+ selectionContextMenu(canvas, menu) {
+ const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0);
+ this.addZOrderingContextMenuItems(canvas, selectedNodes, menu);
+ }
+ addZOrderingContextMenuItems(canvas, nodes, menu) {
+ menu.addSeparator();
+ if (this.plugin.settings.getSetting("zOrderingControlShowOneLayerShiftOptions") && nodes.length === 1) {
+ menu.addItem((item) => {
+ item.setTitle("Move one layer forward");
+ item.setIcon("arrow-up");
+ item.onClick(() => this.moveOneLayer(canvas, nodes.first(), true));
+ });
+ menu.addItem((item) => {
+ item.setTitle("Move one layer backward");
+ item.setIcon("arrow-down");
+ item.onClick(() => this.moveOneLayer(canvas, nodes.first(), false));
+ });
+ }
+ menu.addItem((item) => {
+ item.setTitle("Bring to Front");
+ item.setIcon("bring-to-front");
+ item.onClick(() => this.moveMaxLayers(canvas, nodes, true));
+ });
+ menu.addItem((item) => {
+ item.setTitle("Send to Back");
+ item.setIcon("send-to-back");
+ item.onClick(() => this.moveMaxLayers(canvas, nodes, false));
+ });
+ if (nodes.some((node) => node.getData().zIndex !== void 0)) {
+ menu.addItem((item) => {
+ item.setTitle("Remove persistent z-index");
+ item.setIcon("pin-off");
+ item.onClick(() => this.removePersistentZIndexes(canvas, nodes));
+ });
+ }
+ menu.addSeparator();
+ }
+ moveOneLayer(canvas, selectedNode, forward) {
+ const selectedNodeBBox = selectedNode.getBBox();
+ const collidingNodes = [...canvas.nodes.values()].filter((node) => BBoxHelper.isColliding(selectedNodeBBox, node.getBBox())).filter((node) => node !== selectedNode);
+ const nearestZIndexNode = collidingNodes.sort((a, b) => forward ? a.zIndex - b.zIndex : b.zIndex - a.zIndex).filter((node) => forward ? node.zIndex > selectedNode.zIndex : node.zIndex < selectedNode.zIndex).first();
+ if (nearestZIndexNode === void 0) return;
+ const targetZIndex = nearestZIndexNode.zIndex;
+ this.setNodesZIndex([nearestZIndexNode], selectedNode.zIndex);
+ this.setNodesZIndex([selectedNode], targetZIndex);
+ }
+ moveMaxLayers(canvas, selectedNodes, forward) {
+ const targetZIndex = forward ? Math.max(...this.getAllZIndexes(canvas)) + 1 : Math.min(...this.getAllZIndexes(canvas)) - selectedNodes.length;
+ this.setNodesZIndex(selectedNodes, targetZIndex);
+ }
+ removePersistentZIndexes(_canvas, nodes) {
+ for (const node of nodes) node.setZIndex(void 0);
+ }
+ setNodesZIndex(nodes, zIndex) {
+ const sortedNodes = nodes.sort((a, b) => a.zIndex - b.zIndex);
+ for (let i = 0; i < sortedNodes.length; i++) {
+ const node = sortedNodes[i];
+ const finalZIndex = zIndex + i;
+ node.setZIndex(finalZIndex);
+ }
+ }
+ getAllZIndexes(canvas) {
+ return [...canvas.nodes.values()].map((n) => n.zIndex);
+ }
+};
+
+// src/canvas-extensions/better-readonly-canvas-extension.ts
+var BetterReadonlyCanvasExtension = class extends CanvasExtension {
+ constructor() {
+ super(...arguments);
+ this.isMovingToBBox = false;
+ }
+ isEnabled() {
+ return "betterReadonlyEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.updatePopupMenu(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:viewport-changed:before",
+ (canvas) => this.onBeforeViewPortChanged(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:zoom-to-bbox:before",
+ () => this.isMovingToBBox = true
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:readonly-changed",
+ (canvas, _readonly) => {
+ this.updatePopupMenu(canvas);
+ this.updateLockedZoom(canvas);
+ this.updateLockedPan(canvas);
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.addQuickSettings(canvas)
+ ));
+ }
+ onBeforeViewPortChanged(canvas) {
+ var _a, _b, _c, _d, _e, _f;
+ if (this.isMovingToBBox) {
+ this.isMovingToBBox = false;
+ this.updateLockedZoom(canvas);
+ this.updateLockedPan(canvas);
+ return;
+ }
+ if (!canvas.readonly) return;
+ if (this.plugin.settings.getSetting("disableZoom")) {
+ canvas.zoom = (_a = canvas.lockedZoom) != null ? _a : canvas.zoom;
+ canvas.tZoom = (_b = canvas.lockedZoom) != null ? _b : canvas.tZoom;
+ }
+ if (this.plugin.settings.getSetting("disablePan")) {
+ canvas.x = (_c = canvas.lockedX) != null ? _c : canvas.x;
+ canvas.tx = (_d = canvas.lockedX) != null ? _d : canvas.tx;
+ canvas.y = (_e = canvas.lockedY) != null ? _e : canvas.y;
+ canvas.ty = (_f = canvas.lockedY) != null ? _f : canvas.ty;
+ }
+ }
+ addQuickSettings(canvas) {
+ var _a;
+ const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement;
+ if (!settingsContainer) return;
+ CanvasHelper.addControlMenuButton(
+ settingsContainer,
+ this.createToggle({
+ id: "disable-node-popup",
+ label: "Disable node popup",
+ icon: "arrow-up-right-from-circle",
+ callback: () => this.updatePopupMenu(canvas)
+ }, "disableNodePopup")
+ );
+ CanvasHelper.addControlMenuButton(
+ settingsContainer,
+ this.createToggle({
+ id: "disable-zoom",
+ label: "Disable zoom",
+ icon: "zoom-in",
+ callback: () => this.updateLockedZoom(canvas)
+ }, "disableZoom")
+ );
+ CanvasHelper.addControlMenuButton(
+ settingsContainer,
+ this.createToggle({
+ id: "disable-pan",
+ label: "Disable pan",
+ icon: "move",
+ callback: () => this.updateLockedPan(canvas)
+ }, "disablePan")
+ );
+ }
+ createToggle(menuOption, settingKey) {
+ const toggle = CanvasHelper.createControlMenuButton({
+ ...menuOption,
+ callback: () => (async () => {
+ var _a;
+ const newValue = !this.plugin.settings.getSetting(settingKey);
+ await this.plugin.settings.setSetting({ [settingKey]: newValue });
+ toggle.dataset.toggled = this.plugin.settings.getSetting(settingKey).toString();
+ (_a = menuOption.callback) == null ? void 0 : _a.call(this);
+ })()
+ });
+ toggle.classList.add("show-while-readonly");
+ toggle.dataset.toggled = this.plugin.settings.getSetting(settingKey).toString();
+ return toggle;
+ }
+ updatePopupMenu(canvas) {
+ const hidden = canvas.readonly && this.plugin.settings.getSetting("disableNodePopup");
+ canvas.menu.menuEl.style.visibility = hidden ? "hidden" : "visible";
+ }
+ updateLockedZoom(canvas) {
+ canvas.lockedZoom = canvas.tZoom;
+ }
+ updateLockedPan(canvas) {
+ canvas.lockedX = canvas.tx;
+ canvas.lockedY = canvas.ty;
+ }
+};
+
+// src/canvas-extensions/encapsulate-canvas-extension.ts
+var ENCAPSULATED_FILE_NODE_SIZE = { width: 300, height: 300 };
+var EncapsulateCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "canvasEncapsulationEnabled";
+ }
+ init() {
+ this.plugin.addCommand({
+ id: "encapsulate-selection",
+ name: "Encapsulate selection",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.selection.size > 0,
+ (canvas) => this.encapsulateSelection(canvas)
+ )
+ });
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "canvas:selection-menu",
+ (menu, canvas) => {
+ menu.addItem(
+ (item) => item.setTitle("Encapsulate").setIcon("file-plus").onClick(() => this.encapsulateSelection(canvas))
+ );
+ }
+ ));
+ }
+ async encapsulateSelection(canvas) {
+ var _a, _b, _c, _d;
+ const selection = canvas.getSelectionData();
+ const canvasSettings = this.plugin.app.internalPlugins.plugins.canvas.instance.options;
+ const defaultNewCanvasLocation = canvasSettings.newFileLocation;
+ let targetFolderPath = this.plugin.app.vault.getRoot().path;
+ if (defaultNewCanvasLocation === "current") targetFolderPath = (_c = (_b = (_a = canvas.view.file) == null ? void 0 : _a.parent) == null ? void 0 : _b.path) != null ? _c : targetFolderPath;
+ else if (defaultNewCanvasLocation === "folder") targetFolderPath = (_d = canvasSettings.newFileFolderPath) != null ? _d : targetFolderPath;
+ const targetFilePath = await new FileNameModal(
+ this.plugin.app,
+ targetFolderPath,
+ "canvas"
+ ).awaitInput();
+ const newFileData = { nodes: selection.nodes, edges: selection.edges };
+ const file = await this.plugin.app.vault.create(targetFilePath, JSON.stringify(newFileData, null, 2));
+ for (const nodeData of selection.nodes) {
+ const node = canvas.nodes.get(nodeData.id);
+ if (node) canvas.removeNode(node);
+ }
+ canvas.createFileNode({
+ pos: {
+ x: selection.center.x - ENCAPSULATED_FILE_NODE_SIZE.width / 2,
+ y: selection.center.y - ENCAPSULATED_FILE_NODE_SIZE.height / 2
+ },
+ size: ENCAPSULATED_FILE_NODE_SIZE,
+ file
+ });
+ }
+};
+
+// src/canvas-extensions/commands-canvas-extension.ts
+var import_obsidian14 = require("obsidian");
+var DIRECTIONS = ["up", "down", "left", "right"];
+var CommandsCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "commandsFeatureEnabled";
+ }
+ init() {
+ this.plugin.addCommand({
+ id: "toggle-readonly",
+ name: "Toggle readonly",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => true,
+ (canvas) => canvas.setReadonly(!canvas.readonly)
+ )
+ });
+ this.plugin.addCommand({
+ id: "create-text-node",
+ name: "Create text node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly,
+ (canvas) => this.createTextNode(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "create-file-node",
+ name: "Create file node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly,
+ (canvas) => this.createFileNode(canvas)
+ )
+ });
+ this.plugin.addCommand({
+ id: "select-all-edges",
+ name: "Select all edges",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => true,
+ (canvas) => canvas.updateSelection(
+ () => canvas.selection = new Set(canvas.edges.values())
+ )
+ )
+ });
+ this.plugin.addCommand({
+ id: "zoom-to-selection",
+ name: "Zoom to selection",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => canvas.zoomToSelection()
+ )
+ });
+ this.plugin.addCommand({
+ id: "zoom-to-fit",
+ name: "Zoom to fit",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => true,
+ (canvas) => canvas.zoomToFit()
+ )
+ });
+ for (const direction of DIRECTIONS) {
+ this.plugin.addCommand({
+ id: `clone-node-${direction}`,
+ name: `Clone node ${direction}`,
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => {
+ var _a;
+ return !canvas.readonly && canvas.selection.size === 1 && ((_a = canvas.selection.values().next().value) == null ? void 0 : _a.getData().type) === "text";
+ },
+ (canvas) => this.cloneNode(canvas, direction)
+ )
+ });
+ this.plugin.addCommand({
+ id: `expand-node-${direction}`,
+ name: `Expand node ${direction}`,
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.selection.size === 1,
+ (canvas) => this.expandNode(canvas, direction)
+ )
+ });
+ this.plugin.addCommand({
+ id: `navigate-${direction}`,
+ name: `Navigate ${direction}`,
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.getSelectionData().nodes.length === 1,
+ (canvas) => this.navigate(canvas, direction)
+ )
+ });
+ }
+ this.plugin.addCommand({
+ id: "flip-selection-horizontally",
+ name: "Flip selection horizontally",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.selection.size > 0,
+ (canvas) => this.flipSelection(
+ canvas,
+ true
+ )
+ )
+ });
+ this.plugin.addCommand({
+ id: "flip-selection-vertically",
+ name: "Flip selection vertically",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.selection.size > 0,
+ (canvas) => this.flipSelection(canvas, false)
+ )
+ });
+ this.plugin.addCommand({
+ id: "select-connected-edges",
+ name: "Select connected edges",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "connected")
+ )
+ });
+ this.plugin.addCommand({
+ id: "select-incoming-edges",
+ name: "Select incoming edges",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "incoming")
+ )
+ });
+ this.plugin.addCommand({
+ id: "select-outgoing-edges",
+ name: "Select outgoing edges",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "outgoing")
+ )
+ });
+ this.plugin.addCommand({
+ id: "swap-nodes",
+ name: "Swap nodes",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.getSelectionData().nodes.length === 2,
+ (canvas) => {
+ const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0);
+ if (selectedNodes.length !== 2) return;
+ const [nodeA, nodeB] = selectedNodes;
+ const nodeAData = nodeA.getData();
+ const nodeBData = nodeB.getData();
+ nodeA.setData({ ...nodeAData, x: nodeBData.x, y: nodeBData.y, width: nodeBData.width, height: nodeBData.height });
+ nodeB.setData({ ...nodeBData, x: nodeAData.x, y: nodeAData.y, width: nodeAData.width, height: nodeAData.height });
+ canvas.pushHistory(canvas.getData());
+ }
+ )
+ });
+ this.plugin.addCommand({
+ id: "copy-wikilink-to-node",
+ name: "Copy wikilink to node",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly && canvas.selection.size === 1,
+ (canvas) => {
+ const file = canvas.view.file;
+ if (!file) return;
+ const nodeData = canvas.getSelectionData().nodes[0];
+ if (!nodeData) return;
+ const wikilink = `[[${file.path}#${nodeData.id}|${file.name} (${TextHelper.toTitleCase(nodeData.type)} node)]]`;
+ navigator.clipboard.writeText(wikilink);
+ new import_obsidian14.Notice("Copied wikilink to node to clipboard.", 2e3);
+ }
+ )
+ });
+ this.plugin.addCommand({
+ id: "pull-outgoing-links-to-canvas",
+ name: "Pull outgoing links to canvas",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly,
+ (canvas) => {
+ var _a, _b, _c;
+ const canvasFile = canvas.view.file;
+ if (!canvasFile) return;
+ let selectedNodeIds = canvas.getSelectionData().nodes.map((node) => node.id);
+ if (selectedNodeIds.length === 0) selectedNodeIds = [...canvas.nodes.keys()];
+ const metadata = this.plugin.app.metadataCache.getFileCache(canvasFile);
+ if (!metadata) return;
+ const outgoingLinks = /* @__PURE__ */ new Set();
+ for (const nodeId of selectedNodeIds) {
+ let relativeFile = canvasFile;
+ let nodeOutgoingLinks = (_b = (_a = metadata.nodes) == null ? void 0 : _a[nodeId]) == null ? void 0 : _b.links;
+ if (!nodeOutgoingLinks) {
+ const file = (_c = canvas.nodes.get(nodeId)) == null ? void 0 : _c.file;
+ if (!file) continue;
+ const fileMetadata = this.plugin.app.metadataCache.getFileCache(file);
+ nodeOutgoingLinks = fileMetadata == null ? void 0 : fileMetadata.links;
+ relativeFile = file;
+ }
+ if (!nodeOutgoingLinks) continue;
+ for (const nodeOutgoingLink of nodeOutgoingLinks) {
+ const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(nodeOutgoingLink.link, relativeFile.path);
+ if (!(resolvedLink instanceof import_obsidian14.TFile)) continue;
+ outgoingLinks.add(resolvedLink);
+ }
+ }
+ const existingFileNodes = /* @__PURE__ */ new Set([canvas.view.file]);
+ for (const node of canvas.nodes.values()) {
+ if (node.getData().type !== "file" || !node.file) continue;
+ existingFileNodes.add(node.file);
+ }
+ for (const outgoingLink of outgoingLinks) {
+ if (existingFileNodes.has(outgoingLink)) continue;
+ this.createFileNode(canvas, outgoingLink);
+ }
+ }
+ )
+ });
+ this.plugin.addCommand({
+ id: "pull-backlinks-to-canvas",
+ name: "Pull backlinks to canvas",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => !canvas.readonly,
+ (canvas) => {
+ const canvasFile = canvas.view.file;
+ if (!canvasFile) return;
+ const selectedNodesData = canvas.getSelectionData().nodes.map((node) => node);
+ const backlinks = /* @__PURE__ */ new Set();
+ if (selectedNodesData.length > 0) {
+ for (const nodeData of selectedNodesData) {
+ if (nodeData.type !== "file" || !nodeData.file) continue;
+ const file = this.plugin.app.vault.getFileByPath(nodeData.file);
+ if (!file) continue;
+ const nodeBacklinks = this.plugin.app.metadataCache.getBacklinksForFile(file);
+ if (!nodeBacklinks) continue;
+ for (const nodeBacklink of nodeBacklinks.data.keys()) {
+ const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(nodeBacklink, file.path);
+ if (!(resolvedLink instanceof import_obsidian14.TFile)) continue;
+ backlinks.add(resolvedLink);
+ }
+ }
+ } else {
+ const canvasBacklinks = this.plugin.app.metadataCache.getBacklinksForFile(canvasFile);
+ if (!canvasBacklinks) return;
+ for (const canvasBacklink of canvasBacklinks.data.keys()) {
+ const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(canvasBacklink, canvasFile.path);
+ if (!(resolvedLink instanceof import_obsidian14.TFile)) continue;
+ backlinks.add(resolvedLink);
+ }
+ }
+ const existingFileNodes = /* @__PURE__ */ new Set([canvas.view.file]);
+ for (const node of canvas.nodes.values()) {
+ if (node.getData().type !== "file" || !node.file) continue;
+ existingFileNodes.add(node.file);
+ }
+ for (const backlink of backlinks) {
+ if (existingFileNodes.has(backlink)) continue;
+ this.createFileNode(canvas, backlink);
+ }
+ }
+ )
+ });
+ }
+ createTextNode(canvas) {
+ const size = canvas.config.defaultTextNodeDimensions;
+ const pos = CanvasHelper.getCenterCoordinates(canvas, size);
+ canvas.createTextNode({ pos, size });
+ }
+ async createFileNode(canvas, file) {
+ const size = canvas.config.defaultFileNodeDimensions;
+ const pos = CanvasHelper.getCenterCoordinates(canvas, size);
+ file != null ? file : file = await new FileSelectModal(this.plugin.app, void 0, true).awaitInput();
+ canvas.createFileNode({ pos, size, file });
+ }
+ cloneNode(canvas, cloneDirection) {
+ const sourceNode = canvas.selection.values().next().value;
+ if (!sourceNode) return;
+ const sourceNodeData = sourceNode.getData();
+ const nodeMargin = this.plugin.settings.getSetting("cloneNodeMargin");
+ const offset = {
+ x: (sourceNode.width + nodeMargin) * (cloneDirection === "left" ? -1 : cloneDirection === "right" ? 1 : 0),
+ y: (sourceNode.height + nodeMargin) * (cloneDirection === "up" ? -1 : cloneDirection === "down" ? 1 : 0)
+ };
+ const clonedNode = canvas.createTextNode({
+ pos: {
+ x: sourceNode.x + offset.x,
+ y: sourceNode.y + offset.y
+ },
+ size: {
+ width: sourceNode.width,
+ height: sourceNode.height
+ }
+ });
+ clonedNode.setData({
+ ...clonedNode.getData(),
+ color: sourceNodeData.color,
+ styleAttributes: sourceNodeData.styleAttributes
+ });
+ if (this.plugin.settings.getSetting("zoomToClonedNode"))
+ canvas.zoomToBbox(clonedNode.getBBox());
+ }
+ expandNode(canvas, expandDirection) {
+ const node = canvas.selection.values().next().value;
+ if (!node) return;
+ const expandNodeStepSize = this.plugin.settings.getSetting("expandNodeStepSize");
+ const expand = {
+ x: expandDirection === "left" ? -1 : expandDirection === "right" ? 1 : 0,
+ y: expandDirection === "up" ? -1 : expandDirection === "down" ? 1 : 0
+ };
+ node.setData({
+ ...node.getData(),
+ width: node.width + expand.x * expandNodeStepSize,
+ height: node.height + expand.y * expandNodeStepSize
+ });
+ }
+ flipSelection(canvas, horizontally) {
+ const selectionData = canvas.getSelectionData();
+ if (selectionData.nodes.length === 0) return;
+ const nodeIds = /* @__PURE__ */ new Set();
+ for (const nodeData of selectionData.nodes) {
+ nodeIds.add(nodeData.id);
+ const node = canvas.nodes.get(nodeData.id);
+ if (!node) continue;
+ const newX = horizontally ? 2 * selectionData.center.x - nodeData.x - nodeData.width : nodeData.x;
+ const newY = horizontally ? nodeData.y : 2 * selectionData.center.y - nodeData.y - nodeData.height;
+ node.setData({
+ ...nodeData,
+ x: newX,
+ y: newY
+ });
+ }
+ for (const edge of canvas.edges.values()) {
+ const edgeData = edge.getData();
+ let newFromSide = edgeData.fromSide;
+ if (nodeIds.has(edgeData.fromNode) && BBoxHelper.isHorizontal(edgeData.fromSide) === horizontally)
+ newFromSide = BBoxHelper.getOppositeSide(edgeData.fromSide);
+ let newToSide = edgeData.toSide;
+ if (nodeIds.has(edgeData.toNode) && BBoxHelper.isHorizontal(edgeData.toSide) === horizontally)
+ newToSide = BBoxHelper.getOppositeSide(edgeData.toSide);
+ edge.setData({
+ ...edgeData,
+ fromSide: newFromSide,
+ toSide: newToSide
+ });
+ }
+ canvas.pushHistory(canvas.getData());
+ }
+ navigate(canvas, direction) {
+ const node = this.getNextNode(canvas, direction);
+ if (!node) return;
+ canvas.updateSelection(() => {
+ canvas.selection = /* @__PURE__ */ new Set([node]);
+ });
+ }
+ getNextNode(canvas, direction) {
+ var _a;
+ const selectedNodeData = (_a = canvas.getSelectionData().nodes) == null ? void 0 : _a.first();
+ if (!selectedNodeData) return;
+ const selectedNodeBBox = {
+ minX: selectedNodeData.x,
+ minY: selectedNodeData.y,
+ maxX: selectedNodeData.x + selectedNodeData.width,
+ maxY: selectedNodeData.y + selectedNodeData.height
+ };
+ const possibleTargetNodes = Array.from(canvas.nodes.values()).filter((node) => {
+ const nodeData = node.getData();
+ return nodeData.id !== selectedNodeData.id && (nodeData.type === "text" || nodeData.type === "file");
+ });
+ const closestNode = possibleTargetNodes.reduce((closestNode2, node) => {
+ const nodeBBox = node.getBBox();
+ const isInVerticalRange = selectedNodeBBox.minY <= nodeBBox.maxY && selectedNodeBBox.maxY >= nodeBBox.minY;
+ const isInHorizontalRange = selectedNodeBBox.minX <= nodeBBox.maxX && selectedNodeBBox.maxX >= nodeBBox.minX;
+ if (["up", "down"].includes(direction) && !isInHorizontalRange) return closestNode2;
+ if (["left", "right"].includes(direction) && !isInVerticalRange) return closestNode2;
+ let distance = -1;
+ switch (direction) {
+ case "up":
+ distance = selectedNodeBBox.minY - nodeBBox.maxY;
+ break;
+ case "down":
+ distance = nodeBBox.minY - selectedNodeBBox.maxY;
+ break;
+ case "left":
+ distance = selectedNodeBBox.minX - nodeBBox.maxX;
+ break;
+ case "right":
+ distance = nodeBBox.minX - selectedNodeBBox.maxX;
+ break;
+ }
+ if (distance < 0) return closestNode2;
+ if (!closestNode2) return { node, distance };
+ if (distance < closestNode2.distance) return { node, distance };
+ if (distance === closestNode2.distance) {
+ const selectedNodeCenter = {
+ x: selectedNodeData.x + selectedNodeData.width / 2,
+ y: selectedNodeData.y + selectedNodeData.height / 2
+ };
+ const closestNodeCenter = {
+ x: closestNode2.node.x + closestNode2.node.width / 2,
+ y: closestNode2.node.y + closestNode2.node.height / 2
+ };
+ const nodeCenter = {
+ x: node.x + node.width / 2,
+ y: node.y + node.height / 2
+ };
+ const closestNodeDistance = Math.sqrt(
+ Math.pow(selectedNodeCenter.x - closestNodeCenter.x, 2) + Math.pow(selectedNodeCenter.y - closestNodeCenter.y, 2)
+ );
+ const nodeDistance = Math.sqrt(
+ Math.pow(selectedNodeCenter.x - nodeCenter.x, 2) + Math.pow(selectedNodeCenter.y - nodeCenter.y, 2)
+ );
+ if (nodeDistance < closestNodeDistance) return { node, distance };
+ }
+ return closestNode2;
+ }, null);
+ return closestNode == null ? void 0 : closestNode.node;
+ }
+};
+
+// src/canvas-extensions/auto-resize-node-canvas-extension.ts
+var AutoResizeNodeCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "autoResizeNodeFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-created",
+ (canvas, node) => this.onNodeCreated(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-editing-state-changed",
+ (canvas, node, editing) => this.onNodeEditingStateChanged(canvas, node, editing)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-text-content-changed",
+ (canvas, node, viewUpdate) => this.onNodeTextContentChanged(canvas, node, viewUpdate.view.dom)
+ ));
+ }
+ isValidNodeType(nodeData) {
+ return nodeData.type === "text" || nodeData.type === "file" && nodeData.file.endsWith(".md");
+ }
+ onNodeCreated(_canvas, node) {
+ const autoResizeNodeEnabledByDefault = this.plugin.settings.getSetting("autoResizeNodeEnabledByDefault");
+ if (!autoResizeNodeEnabledByDefault) return;
+ const nodeData = node.getData();
+ if (nodeData.type !== "text" && nodeData.type !== "file") return;
+ node.setData({
+ ...node.getData(),
+ dynamicHeight: true
+ });
+ }
+ onPopupMenuCreated(canvas) {
+ if (canvas.readonly) return;
+ const selectedNodes = canvas.getSelectionData().nodes.filter((nodeData) => this.isValidNodeType(nodeData)).map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0);
+ if (selectedNodes.length === 0) return;
+ const autoResizeHeightEnabled = selectedNodes.some((node) => node.getData().dynamicHeight);
+ CanvasHelper.addPopupMenuOption(
+ canvas,
+ CanvasHelper.createPopupMenuOption({
+ id: "auto-resize-height",
+ label: autoResizeHeightEnabled ? "Disable auto-resize" : "Enable auto-resize",
+ icon: autoResizeHeightEnabled ? "scan-text" : "lock",
+ callback: () => this.toggleAutoResizeHeightEnabled(canvas, selectedNodes, autoResizeHeightEnabled)
+ })
+ );
+ }
+ toggleAutoResizeHeightEnabled(canvas, nodes, autoResizeHeight) {
+ nodes.forEach((node) => node.setData({
+ ...node.getData(),
+ dynamicHeight: !autoResizeHeight
+ }));
+ this.onPopupMenuCreated(canvas);
+ }
+ canBeResized(node) {
+ const nodeData = node.getData();
+ return nodeData.dynamicHeight;
+ }
+ async onNodeEditingStateChanged(_canvas, node, editing) {
+ if (!this.isValidNodeType(node.getData())) return;
+ if (!this.canBeResized(node)) return;
+ await sleep(10);
+ if (editing) {
+ this.onNodeTextContentChanged(_canvas, node, node.child.editMode.cm.dom);
+ return;
+ }
+ const renderedMarkdownContainer = node.nodeEl.querySelector(".markdown-preview-view.markdown-rendered");
+ if (!renderedMarkdownContainer) return;
+ renderedMarkdownContainer.style.height = "min-content";
+ const newHeight = renderedMarkdownContainer.clientHeight;
+ renderedMarkdownContainer.style.removeProperty("height");
+ this.setNodeHeight(node, newHeight);
+ }
+ async onNodeTextContentChanged(_canvas, node, dom) {
+ if (!this.isValidNodeType(node.getData())) return;
+ if (!this.canBeResized(node)) return;
+ const cmScroller = dom.querySelector(".cm-scroller");
+ if (!cmScroller) return;
+ cmScroller.style.height = "min-content";
+ const newHeight = cmScroller.scrollHeight;
+ cmScroller.style.removeProperty("height");
+ this.setNodeHeight(node, newHeight);
+ }
+ setNodeHeight(node, height) {
+ if (height === 0) return;
+ const maxHeight = this.plugin.settings.getSetting("autoResizeNodeMaxHeight");
+ if (maxHeight != -1 && height > maxHeight) height = maxHeight;
+ const nodeData = node.getData();
+ height = Math.max(height, node.canvas.config.minContainerDimension);
+ if (this.plugin.settings.getSetting("autoResizeNodeSnapToGrid"))
+ height = Math.ceil(height / CanvasHelper.GRID_SIZE) * CanvasHelper.GRID_SIZE;
+ node.setData({
+ ...nodeData,
+ height
+ });
+ }
+};
+
+// src/canvas-extensions/portals-canvas-extension.ts
+var import_obsidian15 = require("obsidian");
+var PORTAL_ID_DELIMITER = "||";
+var PORTAL_ID_PREFIX = `acportal${PORTAL_ID_DELIMITER}`;
+var PORTAL_PADDING = 50;
+var MIN_OPEN_PORTAL_SIZE = { width: 200, height: 200 };
+var PortalsCanvasExtension = class _PortalsCanvasExtension extends CanvasExtension {
+ isEnabled() {
+ return "portalsFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.vault.on("modify", (file) => {
+ for (const canvas of this.plugin.getCanvases())
+ this.onFileModified(canvas, file);
+ }));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenu(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-removed",
+ (canvas, node) => this.onNodeRemoved(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-moved",
+ (canvas, node, _keyboard) => this.onNodeMoved(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-resized",
+ (canvas, node) => this.onNodeResized(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:dragging-state-changed",
+ (canvas, startedDragging) => this.onDraggingStateChanged(canvas, startedDragging)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:containing-nodes-requested",
+ (canvas, bbox, nodes) => this.onContainingNodesRequested(canvas, bbox, nodes)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-connection-try-dragging:before",
+ (canvas, edge, event, cancelRef) => this.onEdgeConnectionTryDraggingBefore(canvas, edge, event, cancelRef)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-connection-dragging:after",
+ (canvas, edge, event, newEdge, side, previousEnds) => this.onEdgeConnectionDraggingAfter(canvas, edge, event, newEdge, side, previousEnds)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:selection-changed",
+ (canvas, oldSelection, updateSelection) => this.onSelectionChanged(canvas, oldSelection, updateSelection)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:data-requested",
+ (canvas, data) => this.onGetData(canvas, data)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:data-loaded:before",
+ (canvas, data, setData) => {
+ this.onSetData(canvas, data).then((newData) => {
+ if (newData.nodes.length === data.nodes.length && newData.edges.length === data.edges.length) return;
+ setData(newData);
+ });
+ }
+ ));
+ }
+ onFileModified(canvas, file) {
+ const isAffected = Object.values(canvas.nodes).filter(
+ (nodeData) => nodeData.getData().type === "file" && nodeData.currentPortalFile === file.path
+ ).length > 0;
+ if (!isAffected) return;
+ canvas.setData(canvas.getData());
+ canvas.history.current--;
+ canvas.history.data.pop();
+ }
+ onContainingNodesRequested(_canvas, _bbox, nodes) {
+ const filteredNodes = nodes.filter((node) => !_PortalsCanvasExtension.isPortalElement(node.id));
+ nodes.splice(0, nodes.length, ...filteredNodes);
+ }
+ onSelectionChanged(canvas, _oldSelection, updateSelection) {
+ updateSelection(() => {
+ const updatedSelection = Array.from(canvas.selection).filter((canvasElement) => !_PortalsCanvasExtension.isPortalElement(canvasElement.id));
+ canvas.selection = new Set(updatedSelection);
+ });
+ }
+ onDraggingStateChanged(canvas, startedDragging) {
+ if (!startedDragging) return;
+ if (!canvas.getSelectionData().nodes.some((node) => node.type === "file" && node.portal)) return;
+ const objectSnappingEnabled = canvas.options.snapToObjects;
+ if (!objectSnappingEnabled) return;
+ canvas.toggleObjectSnapping(false);
+ const dragEndEventRef = this.plugin.app.workspace.on(
+ "advanced-canvas:dragging-state-changed",
+ (canvas2, startedDragging2) => {
+ if (startedDragging2) return;
+ canvas2.toggleObjectSnapping(objectSnappingEnabled);
+ this.plugin.app.workspace.offref(dragEndEventRef);
+ }
+ );
+ this.plugin.registerEvent(dragEndEventRef);
+ }
+ onNodeMoved(canvas, portalNode) {
+ const portalNodeData = portalNode.getData();
+ if (portalNodeData.type !== "file" || !portalNodeData.isPortalLoaded) return;
+ const nestedNodes = this.getContainingNodes(canvas, portalNode);
+ const containingNodesBBox = CanvasHelper.getBBox(nestedNodes);
+ const portalOffset = {
+ x: portalNodeData.x - containingNodesBBox.minX + PORTAL_PADDING,
+ y: portalNodeData.y - containingNodesBBox.minY + PORTAL_PADDING
+ };
+ for (const nestedNode of nestedNodes) {
+ const nestedNodeData = nestedNode.getData();
+ nestedNode.setData({
+ ...nestedNodeData,
+ x: nestedNodeData.x + portalOffset.x,
+ y: nestedNodeData.y + portalOffset.y
+ });
+ }
+ }
+ onNodeResized(_canvas, portalNode) {
+ const portalNodeData = portalNode.getData();
+ if (portalNodeData.type !== "file" || !portalNodeData.isPortalLoaded) return;
+ portalNode.setData({
+ ...portalNodeData,
+ x: portalNode.prevX ? portalNode.prevX : portalNodeData.x,
+ y: portalNode.prevY ? portalNode.prevY : portalNodeData.y,
+ width: portalNode.prevWidth ? portalNode.prevWidth : portalNodeData.width,
+ height: portalNode.prevHeight ? portalNode.prevHeight : portalNodeData.height
+ });
+ }
+ onNodeRemoved(canvas, portalNode) {
+ const portalNodeData = portalNode.getData();
+ if (portalNodeData.type !== "file" || !portalNodeData.portal) return;
+ for (const node of this.getContainingNodes(canvas, portalNode, false))
+ canvas.removeNode(node);
+ for (const edge of this.getContainingEdges(canvas, portalNode, false))
+ canvas.removeEdge(edge);
+ }
+ onEdgeConnectionTryDraggingBefore(_canvas, edge, _event, cancelRef) {
+ if (!_PortalsCanvasExtension.isPortalElement(edge.id)) return;
+ cancelRef.value = true;
+ new import_obsidian15.Notice("Updating edges from portals is not supported yet.");
+ }
+ onEdgeConnectionDraggingAfter(canvas, edge, _event, _newEdge, _side, _previousEnds) {
+ if (_PortalsCanvasExtension.isPortalElement(edge.id)) return;
+ if (!_PortalsCanvasExtension.isPortalElement(edge.from.node.id) || !_PortalsCanvasExtension.isPortalElement(edge.to.node.id)) return;
+ canvas.removeEdge(edge);
+ new import_obsidian15.Notice("Creating edges with both ends in portals are not supported yet.");
+ }
+ onPopupMenu(canvas) {
+ if (canvas.readonly) return;
+ const selectedFileNodes = canvas.getSelectionData().nodes.map((nodeData) => {
+ var _a;
+ const node = canvas.nodes.get(nodeData.id);
+ if (!node) return null;
+ if (nodeData.type !== "file") return null;
+ if (((_a = node.file) == null ? void 0 : _a.extension) === "canvas") return node;
+ if (nodeData.portal) this.setPortalOpen(canvas, node, false);
+ return null;
+ }).filter((node) => node !== null);
+ if (selectedFileNodes.length !== 1) return;
+ const portalNode = selectedFileNodes.first();
+ const portalNodeData = portalNode.getData();
+ if (portalNodeData.portal && portalNodeData.file !== portalNode.currentPortalFile)
+ this.setPortalOpen(canvas, portalNode, true);
+ CanvasHelper.addPopupMenuOption(
+ canvas,
+ CanvasHelper.createPopupMenuOption({
+ id: "toggle-portal",
+ label: portalNodeData.portal ? "Close portal" : "Open portal",
+ icon: portalNodeData.portal ? "door-open" : "door-closed",
+ callback: () => {
+ this.setPortalOpen(canvas, portalNode, !portalNodeData.portal);
+ this.onPopupMenu(canvas);
+ }
+ })
+ );
+ }
+ setPortalOpen(canvas, portalNode, open) {
+ const portalNodeData = portalNode.getData();
+ portalNode.setData({
+ ...portalNodeData,
+ portal: open
+ });
+ portalNode.currentPortalFile = open ? portalNodeData.file : void 0;
+ canvas.setData(canvas.getData());
+ }
+ // Remove all edges and nodes from portals
+ onGetData(_canvas, data) {
+ data.nodes = data.nodes.filter((nodeData) => !_PortalsCanvasExtension.isPortalElement(nodeData.id));
+ for (const nodeData of data.nodes) delete nodeData.isPortalLoaded;
+ const portalsIdMap = new Map(
+ data.nodes.filter((nodeData) => nodeData.portal).map((nodeData) => [nodeData.id, nodeData])
+ );
+ data.edges = data.edges.filter((edgeData) => {
+ var _a;
+ if (_PortalsCanvasExtension.isPortalElement(edgeData.id)) return false;
+ const isFromNodeFromPortal = _PortalsCanvasExtension.getNestedIds(edgeData.fromNode).length > 1;
+ const isToNodeFromPortal = _PortalsCanvasExtension.getNestedIds(edgeData.toNode).length > 1;
+ if (!isFromNodeFromPortal && !isToNodeFromPortal) return true;
+ if (isFromNodeFromPortal && isToNodeFromPortal) return false;
+ const targetPortalId = this.getParentPortalId(isFromNodeFromPortal ? edgeData.fromNode : edgeData.toNode);
+ const targetPortalData = portalsIdMap.get(targetPortalId);
+ if (!targetPortalData) return false;
+ (_a = targetPortalData.interdimensionalEdges) != null ? _a : targetPortalData.interdimensionalEdges = [];
+ targetPortalData.interdimensionalEdges.push(edgeData);
+ return false;
+ });
+ }
+ // Add all edges and nodes from portals
+ async onSetData(canvas, dataRef) {
+ if (!(dataRef == null ? void 0 : dataRef.nodes)) return dataRef;
+ const data = JSON.parse(JSON.stringify(dataRef));
+ const addedData = await Promise.all(data.nodes.map((nodeData) => this.tryOpenPortal(canvas, nodeData)));
+ for (const newData of addedData) {
+ data.nodes.push(...newData.nodes);
+ data.edges.push(...newData.edges);
+ }
+ for (const nodeData of data.nodes) {
+ if (nodeData.type !== "file" || !nodeData.isPortalLoaded) continue;
+ const interdimensionalEdges = nodeData.interdimensionalEdges;
+ if (!interdimensionalEdges) continue;
+ for (const edge of interdimensionalEdges) data.edges.push(edge);
+ delete nodeData.interdimensionalEdges;
+ }
+ return data;
+ }
+ async tryOpenPortal(canvas, portalNodeData, nestedPortalFiles = /* @__PURE__ */ new Set()) {
+ const addedData = { nodes: [], edges: [] };
+ if (portalNodeData.type !== "file" || !portalNodeData.portal) return addedData;
+ if (portalNodeData.file === canvas.view.file.path) return addedData;
+ if (nestedPortalFiles.has(portalNodeData.file)) return addedData;
+ nestedPortalFiles.add(portalNodeData.file);
+ const portalFile = this.plugin.app.vault.getAbstractFileByPath(portalNodeData.file);
+ if (!(portalFile instanceof import_obsidian15.TFile) || portalFile.extension !== "canvas") return addedData;
+ const portalFileDataString = await this.plugin.app.vault.cachedRead(portalFile);
+ if (portalFileDataString === "") return addedData;
+ const portalFileData = JSON.parse(portalFileDataString);
+ if (!portalFileData) return addedData;
+ portalNodeData.isPortalLoaded = true;
+ const sourceMinCoordinates = CanvasHelper.getBBox(portalFileData.nodes);
+ const portalOffset = {
+ x: portalNodeData.x - sourceMinCoordinates.minX + PORTAL_PADDING,
+ y: portalNodeData.y - sourceMinCoordinates.minY + PORTAL_PADDING
+ };
+ for (const nodeDataFromPortal of portalFileData.nodes) {
+ let newNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${nodeDataFromPortal.id}`;
+ if (!newNodeId.startsWith(PORTAL_ID_PREFIX))
+ newNodeId = PORTAL_ID_PREFIX + newNodeId;
+ const addedNode = {
+ ...nodeDataFromPortal,
+ id: newNodeId,
+ x: nodeDataFromPortal.x + portalOffset.x,
+ y: nodeDataFromPortal.y + portalOffset.y
+ };
+ addedData.nodes.push(addedNode);
+ const nestedNodes = await this.tryOpenPortal(canvas, addedNode, nestedPortalFiles);
+ addedData.nodes.push(...nestedNodes.nodes);
+ addedData.edges.push(...nestedNodes.edges);
+ }
+ for (const edgeDataFromPortal of portalFileData.edges) {
+ let newEdgeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.id}`;
+ if (!newEdgeId.startsWith(PORTAL_ID_PREFIX))
+ newEdgeId = PORTAL_ID_PREFIX + newEdgeId;
+ let fromNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.fromNode}`;
+ if (!fromNodeId.startsWith(PORTAL_ID_PREFIX))
+ fromNodeId = PORTAL_ID_PREFIX + fromNodeId;
+ let toNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.toNode}`;
+ if (!toNodeId.startsWith(PORTAL_ID_PREFIX))
+ toNodeId = PORTAL_ID_PREFIX + toNodeId;
+ addedData.edges.push({
+ ...edgeDataFromPortal,
+ id: newEdgeId,
+ fromNode: fromNodeId,
+ toNode: toNodeId
+ });
+ }
+ const targetSize = this.getPortalSize(CanvasHelper.getBBox(addedData.nodes));
+ portalNodeData.width = targetSize.width;
+ portalNodeData.height = targetSize.height;
+ return addedData;
+ }
+ // Helper functions
+ getPortalSize(sourceBBox) {
+ const sourceSize = {
+ width: sourceBBox.maxX - sourceBBox.minX,
+ height: sourceBBox.maxY - sourceBBox.minY
+ };
+ const targetSize = {
+ width: Math.max(sourceSize.width + PORTAL_PADDING * 2, MIN_OPEN_PORTAL_SIZE.width),
+ height: Math.max(sourceSize.height + PORTAL_PADDING * 2, MIN_OPEN_PORTAL_SIZE.height)
+ };
+ if (!Number.isFinite(targetSize.width)) targetSize.width = MIN_OPEN_PORTAL_SIZE.width;
+ if (!Number.isFinite(targetSize.height)) targetSize.height = MIN_OPEN_PORTAL_SIZE.height;
+ return targetSize;
+ }
+ getContainingNodes(canvas, portalNode, directChildren = true) {
+ return Array.from(canvas.nodes.values()).filter((node) => this.isChildOfPortal(portalNode.getData(), node.getData(), directChildren));
+ }
+ getContainingEdges(canvas, portalNode, directChildren = true) {
+ return Array.from(canvas.edges.values()).filter((edge) => this.isChildOfPortal(portalNode.getData(), edge.getData(), directChildren));
+ }
+ getParentPortalId(elementId) {
+ const nestedIds = _PortalsCanvasExtension.getNestedIds(elementId);
+ if (nestedIds.length < 2) return void 0;
+ return nestedIds.slice(0, -1).join(PORTAL_ID_DELIMITER);
+ }
+ static getNestedIds(id) {
+ if (!this.isPortalElement(id)) return [id];
+ const trimmedId = id.substring(PORTAL_ID_PREFIX.length);
+ return trimmedId.split(PORTAL_ID_DELIMITER);
+ }
+ static isPortalElement(id) {
+ return id.startsWith(PORTAL_ID_PREFIX);
+ }
+ isChildOfPortal(portal, canvasElement, directChild = true) {
+ const nestedIds = _PortalsCanvasExtension.getNestedIds(canvasElement.id);
+ if (nestedIds.length < 2) return false;
+ return canvasElement.id !== portal.id && // Not the portal itself
+ nestedIds.contains(portal.id) && // Is a child of the portal
+ (!directChild || nestedIds[nestedIds.length - 2] === portal.id);
+ }
+};
+
+// src/canvas-extensions/frontmatter-control-button-canvas-extension.ts
+var import_obsidian16 = require("obsidian");
+var FrontmatterControlButtonCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "canvasMetadataCompatibilityEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.addQuickSettings(canvas)
+ ));
+ }
+ addQuickSettings(canvas) {
+ var _a;
+ if (!canvas) return;
+ const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement;
+ if (!settingsContainer) return;
+ CanvasHelper.addControlMenuButton(
+ settingsContainer,
+ CanvasHelper.createControlMenuButton({
+ id: "properties-button",
+ icon: "info",
+ label: "Properties",
+ callback: () => {
+ var _a2;
+ const propertiesPlugin = this.plugin.app.internalPlugins.plugins["properties"];
+ if (!(propertiesPlugin == null ? void 0 : propertiesPlugin._loaded)) {
+ new import_obsidian16.Notice(`Core plugin "Properties view" was not found or isn't enabled. Enable it and restart Obsidian.`);
+ return;
+ }
+ let propertiesLeaf = (_a2 = this.plugin.app.workspace.getLeavesOfType("file-properties").first()) != null ? _a2 : null;
+ if (!propertiesLeaf) {
+ propertiesLeaf = this.plugin.app.workspace.getRightLeaf(false);
+ propertiesLeaf == null ? void 0 : propertiesLeaf.setViewState({ type: "file-properties" });
+ }
+ if (propertiesLeaf) this.plugin.app.workspace.revealLeaf(propertiesLeaf);
+ }
+ })
+ );
+ }
+};
+
+// src/canvas-extensions/better-default-settings-canvas-extension.ts
+var BetterDefaultSettingsCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return true;
+ }
+ init() {
+ this.modifyCanvasSettings(this.plugin.getCurrentCanvas());
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:settings-changed",
+ () => this.modifyCanvasSettings(this.plugin.getCurrentCanvas())
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.modifyCanvasSettings(canvas)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:double-click",
+ (canvas, event, preventDefault) => this.onDoubleClick(canvas, event, preventDefault)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-created",
+ (canvas, node) => {
+ this.enforceNodeGridAlignment(canvas, node);
+ this.applyDefaultNodeStyles(canvas, node);
+ }
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:edge-created",
+ (canvas, edge) => this.applyDefaultEdgeStyles(canvas, edge)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-resized",
+ (canvas, node) => this.enforceMaxNodeWidth(canvas, node)
+ ));
+ }
+ modifyCanvasSettings(canvas) {
+ if (!canvas) return;
+ const defaultTextNodeDimensionsArray = this.plugin.settings.getSetting("defaultTextNodeDimensions");
+ canvas.config.defaultTextNodeDimensions = {
+ width: defaultTextNodeDimensionsArray[0],
+ height: defaultTextNodeDimensionsArray[1]
+ };
+ const defaultFileNodeDimensionsArray = this.plugin.settings.getSetting("defaultFileNodeDimensions");
+ canvas.config.defaultFileNodeDimensions = {
+ width: defaultFileNodeDimensionsArray[0],
+ height: defaultFileNodeDimensionsArray[1]
+ };
+ canvas.config.minContainerDimension = this.plugin.settings.getSetting("minNodeSize");
+ }
+ async onDoubleClick(canvas, event, preventDefault) {
+ if (event.defaultPrevented || event.target !== canvas.wrapperEl || canvas.isDragging || canvas.readonly) return;
+ preventDefault.value = true;
+ const pos = canvas.posFromEvt(event);
+ switch (this.plugin.settings.getSetting("nodeTypeOnDoubleClick")) {
+ case "file": {
+ const file = await new FileSelectModal(this.plugin.app, void 0, true).awaitInput();
+ canvas.createFileNode({
+ pos,
+ position: "center",
+ file
+ });
+ break;
+ }
+ default:
+ canvas.createTextNode({
+ pos,
+ position: "center"
+ });
+ break;
+ }
+ }
+ enforceNodeGridAlignment(_canvas, node) {
+ if (!this.plugin.settings.getSetting("alignNewNodesToGrid")) return;
+ const nodeData = node.getData();
+ node.setData({
+ ...nodeData,
+ x: CanvasHelper.alignToGrid(nodeData.x),
+ y: CanvasHelper.alignToGrid(nodeData.y)
+ });
+ }
+ applyDefaultNodeStyles(_canvas, node) {
+ const nodeData = node.getData();
+ if (nodeData.type !== "text") return;
+ let color = this.plugin.settings.getSetting("defaultTextNodeColor").toString();
+ if (color === "0") color = void 0;
+ node.setData({
+ ...nodeData,
+ color,
+ styleAttributes: {
+ ...nodeData.styleAttributes,
+ ...this.plugin.settings.getSetting("defaultTextNodeStyleAttributes")
+ }
+ });
+ }
+ async applyDefaultEdgeStyles(canvas, edge) {
+ var _a;
+ const edgeData = edge.getData();
+ let color = this.plugin.settings.getSetting("defaultEdgeColor").toString();
+ if (this.plugin.settings.getSetting("inheritEdgeColorFromNode"))
+ color = (_a = edge.from.node.getData().color) != null ? _a : color;
+ if (color === "0") color = void 0;
+ edge.setData({
+ ...edgeData,
+ color,
+ styleAttributes: {
+ ...edgeData.styleAttributes,
+ ...this.plugin.settings.getSetting("defaultEdgeStyleAttributes")
+ }
+ });
+ if (canvas.canvasEl.hasClass("is-connecting")) {
+ await new Promise((resolve) => {
+ new MutationObserver(() => {
+ if (!canvas.canvasEl.hasClass("is-connecting")) resolve();
+ }).observe(canvas.canvasEl, { attributes: true, attributeFilter: ["class"] });
+ });
+ }
+ const lineDirection = this.plugin.settings.getSetting("defaultEdgeLineDirection");
+ edge.setData({
+ ...edge.getData(),
+ fromEnd: lineDirection === "bidirectional" ? "arrow" : "none",
+ toEnd: lineDirection === "nondirectional" ? "none" : "arrow"
+ });
+ }
+ enforceMaxNodeWidth(_canvas, node) {
+ const maxNodeWidth = this.plugin.settings.getSetting("maxNodeWidth");
+ if (maxNodeWidth <= 0) return;
+ const nodeData = node.getData();
+ if (nodeData.type !== "text" && nodeData.type !== "file" || nodeData.portal) return;
+ if (nodeData.width <= maxNodeWidth) return;
+ node.setData({
+ ...nodeData,
+ x: node.prevX !== void 0 ? node.prevX : nodeData.x,
+ // Reset the position to the previous value
+ width: maxNodeWidth
+ });
+ }
+};
+
+// src/canvas-extensions/color-palette-canvas-extension.ts
+var DEFAULT_COLORS_COUNT = 6;
+var CUSTOM_COLORS_MOD_STYLES_ID = "mod-custom-colors";
+var ColorPaletteCanvasExtension = class extends CanvasExtension {
+ constructor() {
+ super(...arguments);
+ this.observer = null;
+ }
+ isEnabled() {
+ return true;
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "window-open",
+ (_win, _window) => this.updateCustomColorModStyleClasses()
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "css-change",
+ () => this.updateCustomColorModStyleClasses()
+ ));
+ this.updateCustomColorModStyleClasses();
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.patchColorSelection(canvas)
+ ));
+ this.plugin.register(() => {
+ var _a;
+ return (_a = this.observer) == null ? void 0 : _a.disconnect();
+ });
+ }
+ updateCustomColorModStyleClasses() {
+ var _a;
+ const customCss = this.getCustomColors().map((colorId) => `
+ .mod-canvas-color-${colorId} {
+ --canvas-color: var(--canvas-color-${colorId});
+ }
+ `).join("");
+ for (const win of this.plugin.windowsManager.windows) {
+ const doc = win.document;
+ (_a = doc.getElementById(CUSTOM_COLORS_MOD_STYLES_ID)) == null ? void 0 : _a.remove();
+ const customColorModStyle = doc.createElement("style");
+ customColorModStyle.id = CUSTOM_COLORS_MOD_STYLES_ID;
+ doc.head.appendChild(customColorModStyle);
+ customColorModStyle.textContent = customCss;
+ }
+ }
+ patchColorSelection(canvas) {
+ if (this.observer) this.observer.disconnect();
+ this.observer = new MutationObserver((mutations) => {
+ const colorMenuOpened = mutations.some(
+ (mutation) => Object.values(mutation.addedNodes).some(
+ (node) => node instanceof HTMLElement && node.classList.contains("canvas-submenu") && Object.values(node.childNodes).some(
+ (node2) => node2 instanceof HTMLElement && node2.classList.contains("canvas-color-picker-item")
+ )
+ )
+ );
+ if (!colorMenuOpened) return;
+ const submenu = canvas.menu.menuEl.querySelector(".canvas-submenu");
+ if (!submenu) return;
+ const currentNodeColor = canvas.getSelectionData().nodes.map((node) => node.color).last();
+ for (const colorId of this.getCustomColors()) {
+ const customColorMenuItem = this.createColorMenuItem(canvas, colorId);
+ if (currentNodeColor === colorId) customColorMenuItem.classList.add("is-active");
+ submenu.insertBefore(customColorMenuItem, submenu.lastChild);
+ }
+ });
+ this.observer.observe(canvas.menu.menuEl, { childList: true });
+ }
+ createColorMenuItem(canvas, colorId) {
+ const menuItem = document.createElement("div");
+ menuItem.classList.add("canvas-color-picker-item");
+ menuItem.classList.add(`mod-canvas-color-${colorId}`);
+ menuItem.addEventListener("click", () => {
+ menuItem.classList.add("is-active");
+ for (const item of canvas.selection) {
+ item.setColor(colorId);
+ }
+ canvas.requestSave();
+ });
+ return menuItem;
+ }
+ getCustomColors() {
+ const colors = [];
+ const style = getComputedStyle(document.body);
+ let colorIndex = DEFAULT_COLORS_COUNT + 1;
+ while (style.getPropertyValue(`--canvas-color-${colorIndex}`)) {
+ colors.push(colorIndex.toString());
+ colorIndex++;
+ }
+ return colors;
+ }
+};
+
+// src/canvas-extensions/collapsible-groups-canvas-extension.ts
+var import_obsidian17 = require("obsidian");
+var CollapsibleGroupsCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "collapsibleGroupsFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-changed",
+ (canvas, node) => this.onNodeChanged(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-bbox-requested",
+ (canvas, node, bbox) => this.onNodeBBoxRequested(canvas, node, bbox)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:copy",
+ (canvas, selectionData) => this.onCopy(canvas, selectionData)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:data-requested",
+ (_canvas, data) => this.expandNodes(data)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:data-loaded:before",
+ (_canvas, data, _setData) => this.collapseNodes(data)
+ ));
+ this.plugin.addCommand({
+ id: "toggle-collapse-group",
+ name: "Toggle collapse group",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size === 1 && canvas.selection.values().next().value.getData().type === "group",
+ (canvas) => this.toggleCollapseGroup(canvas, canvas.selection.values().next().value)
+ )
+ });
+ }
+ toggleCollapseGroup(canvas, node) {
+ const data = node.getData();
+ this.setCollapsed(node.canvas, node, data.collapsed ? void 0 : true);
+ canvas.markMoved(node);
+ }
+ onNodeChanged(canvas, groupNode) {
+ var _a, _b;
+ const groupNodeData = groupNode.getData();
+ if (groupNodeData.type !== "group") return;
+ (_a = groupNode.collapseEl) == null ? void 0 : _a.remove();
+ const collapseEl = document.createElement("span");
+ collapseEl.className = "collapse-button";
+ (0, import_obsidian17.setIcon)(collapseEl, groupNodeData.collapsed ? "plus-circle" : "minus-circle");
+ collapseEl.onclick = () => this.toggleCollapseGroup(canvas, groupNode);
+ groupNode.collapseEl = collapseEl;
+ (_b = groupNode.labelEl) == null ? void 0 : _b.insertAdjacentElement("afterend", collapseEl);
+ }
+ onCopy(_canvas, selectionData) {
+ for (const collapsedGroupData of selectionData.nodes) {
+ if (collapsedGroupData.type !== "group" || !collapsedGroupData.collapsed || !collapsedGroupData.collapsedData) continue;
+ selectionData.nodes.push(...collapsedGroupData.collapsedData.nodes.map((nodeData) => ({
+ ...nodeData,
+ // Restore the relative position of the node to the group
+ x: nodeData.x + collapsedGroupData.x,
+ y: nodeData.y + collapsedGroupData.y
+ })));
+ selectionData.edges.push(...collapsedGroupData.collapsedData.edges);
+ }
+ }
+ setCollapsed(canvas, groupNode, collapsed) {
+ groupNode.setData({ ...groupNode.getData(), collapsed });
+ canvas.setData(canvas.getData());
+ canvas.history.current--;
+ canvas.pushHistory(canvas.getData());
+ canvas.requestSave();
+ }
+ onNodeBBoxRequested(canvas, node, bbox) {
+ var _a, _b;
+ const nodeData = node.getData();
+ if (nodeData.type !== "group" || !nodeData.collapsed) return;
+ const collapseElBBox = (_a = node.collapseEl) == null ? void 0 : _a.getBoundingClientRect();
+ if (!collapseElBBox) return;
+ const labelElBBox = (_b = node.labelEl) == null ? void 0 : _b.getBoundingClientRect();
+ if (!labelElBBox) return;
+ const minPos = canvas.posFromClient({ x: collapseElBBox.left, y: collapseElBBox.top });
+ const maxPos = canvas.posFromClient({ x: labelElBBox.right, y: collapseElBBox.bottom });
+ bbox.minX = minPos.x;
+ bbox.minY = minPos.y;
+ bbox.maxX = maxPos.x;
+ bbox.maxY = maxPos.y;
+ }
+ expandNodes(data) {
+ var _a;
+ if (!data) return;
+ data.nodes = (_a = data.nodes) == null ? void 0 : _a.flatMap((groupNodeData) => {
+ const collapsedData = groupNodeData.collapsedData;
+ if (collapsedData === void 0) return [groupNodeData];
+ delete groupNodeData.collapsedData;
+ data.edges.push(...collapsedData.edges);
+ return [groupNodeData, ...collapsedData.nodes.map((nodeData) => ({
+ ...nodeData,
+ // Restore the relative position of the node to the group
+ x: nodeData.x + groupNodeData.x,
+ y: nodeData.y + groupNodeData.y
+ }))];
+ });
+ }
+ collapseNodes(data) {
+ var _a;
+ (_a = data == null ? void 0 : data.nodes) == null ? void 0 : _a.forEach((groupNodeData) => {
+ var _a2, _b, _c, _d;
+ if (!groupNodeData.collapsed) return;
+ const groupNodeBBox = CanvasHelper.getBBox([groupNodeData]);
+ const containedNodesData = data.nodes.filter(
+ (nodeData) => nodeData.id !== groupNodeData.id && BBoxHelper.insideBBox(CanvasHelper.getBBox([nodeData]), groupNodeBBox, false)
+ );
+ const containedEdgesData = data.edges.filter((edgeData) => {
+ return containedNodesData.some((nodeData) => nodeData.id === edgeData.fromNode) || containedNodesData.some((nodeData) => nodeData.id === edgeData.toNode);
+ });
+ data.nodes = data.nodes.filter((nodeData) => !containedNodesData.includes(nodeData));
+ data.edges = data.edges.filter((edgeData) => !containedEdgesData.includes(edgeData));
+ const newContainedNodesData = containedNodesData.filter((nodeData) => {
+ var _a3, _b2, _c2;
+ return !((_c2 = (_b2 = (_a3 = groupNodeData.collapsedData) == null ? void 0 : _a3.nodes) == null ? void 0 : _b2.some((e) => e.id === nodeData.id)) != null ? _c2 : false);
+ });
+ const newContainedEdgesData = containedEdgesData.filter((edgeData) => {
+ var _a3, _b2, _c2;
+ return !((_c2 = (_b2 = (_a3 = groupNodeData.collapsedData) == null ? void 0 : _a3.edges) == null ? void 0 : _b2.some((n) => n.id === edgeData.id)) != null ? _c2 : false);
+ });
+ groupNodeData.collapsedData = {
+ nodes: [
+ ...(_b = (_a2 = groupNodeData.collapsedData) == null ? void 0 : _a2.nodes) != null ? _b : [],
+ ...newContainedNodesData.map((nodeData) => ({
+ ...nodeData,
+ // Store the relative position of the node to the group
+ x: nodeData.x - groupNodeData.x,
+ y: nodeData.y - groupNodeData.y
+ }))
+ ],
+ edges: [
+ ...(_d = (_c = groupNodeData.collapsedData) == null ? void 0 : _c.edges) != null ? _d : [],
+ ...newContainedEdgesData
+ ]
+ };
+ });
+ }
+};
+
+// src/canvas-extensions/focus-mode-canvas-extension.ts
+var CONTROL_MENU_FOCUS_TOGGLE_ID = "focus-mode-toggle";
+var FocusModeCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "focusModeFeatureEnabled";
+ }
+ init() {
+ this.plugin.addCommand({
+ id: "toggle-focus-mode",
+ name: "Toggle Focus Mode",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (_canvas) => true,
+ (canvas) => this.toggleFocusMode(canvas)
+ )
+ });
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:canvas-changed",
+ (canvas) => this.addControlMenuToggle(canvas)
+ ));
+ }
+ addControlMenuToggle(canvas) {
+ var _a;
+ const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement;
+ if (!settingsContainer) return;
+ const controlMenuFocusToggle = CanvasHelper.createControlMenuButton({
+ id: CONTROL_MENU_FOCUS_TOGGLE_ID,
+ label: "Focus Mode",
+ icon: "focus",
+ callback: () => this.toggleFocusMode(canvas)
+ });
+ CanvasHelper.addControlMenuButton(settingsContainer, controlMenuFocusToggle);
+ }
+ toggleFocusMode(canvas) {
+ var _a, _b;
+ const controlMenuFocusToggle = (_b = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement) == null ? void 0 : _b.querySelector(`#${CONTROL_MENU_FOCUS_TOGGLE_ID}`);
+ if (!controlMenuFocusToggle) return;
+ const newValue = controlMenuFocusToggle.dataset.toggled !== "true";
+ canvas.wrapperEl.dataset.focusModeEnabled = newValue.toString();
+ controlMenuFocusToggle.dataset.toggled = newValue.toString();
+ }
+};
+
+// src/canvas-extensions/auto-file-node-edges-canvas-extension.ts
+var AUTO_EDGE_ID_PREFIX = "afe";
+var AutoFileNodeEdgesCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "autoFileNodeEdgesFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.metadataCache.on("changed", (file) => {
+ for (const canvas of this.plugin.getCanvases())
+ this.onMetadataChanged(canvas, file);
+ }));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-added",
+ (canvas, node) => this.onNodeChanged(canvas, node)
+ ));
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-changed",
+ (canvas, node) => this.onNodeChanged(canvas, node)
+ ));
+ }
+ onMetadataChanged(canvas, file) {
+ var _a;
+ for (const node of canvas.nodes.values()) {
+ if (node.getData().type !== "file" || ((_a = node.file) == null ? void 0 : _a.path) !== file.path) continue;
+ this.updateFileNodeEdges(canvas, node);
+ }
+ }
+ onNodeChanged(canvas, node) {
+ if (node.getData().type !== "file") return;
+ for (const node2 of canvas.nodes.values()) {
+ if (node2.getData().type !== "file") continue;
+ this.updateFileNodeEdges(canvas, node2);
+ }
+ }
+ updateFileNodeEdges(canvas, node) {
+ const edges = this.getFileNodeEdges(canvas, node);
+ const newEdges = Array.from(edges.values()).filter((edge) => !canvas.edges.has(edge.id));
+ canvas.importData({ nodes: [], edges: newEdges }, false, false);
+ for (const edge of canvas.edges.values()) {
+ if (edge.id.startsWith(`${AUTO_EDGE_ID_PREFIX}${node.id}`) && !edges.has(edge.id))
+ canvas.removeEdge(edge);
+ }
+ }
+ getFileNodeEdges(canvas, node) {
+ var _a, _b;
+ const canvasFile = canvas.view.file;
+ if (!canvasFile || !node.file) return /* @__PURE__ */ new Map();
+ const fileMetadata = this.plugin.app.metadataCache.getFileCache(node.file);
+ if (!fileMetadata) return /* @__PURE__ */ new Map();
+ const linkedFilesFrontmatterKey = this.plugin.settings.getSetting("autoFileNodeEdgesFrontmatterKey");
+ const fileLinksToBeLinkedTo = (_b = (_a = fileMetadata.frontmatterLinks) == null ? void 0 : _a.filter((link) => link.key.split(".")[0] === linkedFilesFrontmatterKey)) != null ? _b : [];
+ const filepathsToBeLinkedTo = fileLinksToBeLinkedTo.map((link) => this.plugin.app.metadataCache.getFirstLinkpathDest(link.link, canvasFile.path)).map((file) => file == null ? void 0 : file.path).filter((path) => path !== null);
+ const nodesToBeLinkedTo = Array.from(canvas.nodes.values()).filter((otherNode) => {
+ var _a2;
+ return otherNode.id !== node.id && filepathsToBeLinkedTo.includes((_a2 = otherNode.file) == null ? void 0 : _a2.path);
+ });
+ const newEdges = /* @__PURE__ */ new Map();
+ for (const otherNode of nodesToBeLinkedTo) {
+ const edgeId = `${AUTO_EDGE_ID_PREFIX}${node.id}${otherNode.id}`;
+ const bestFromSide = CanvasHelper.getBestSideForFloatingEdge(BBoxHelper.getCenterOfBBoxSide(otherNode.getBBox(), "right"), node);
+ const bestToSide = CanvasHelper.getBestSideForFloatingEdge(BBoxHelper.getCenterOfBBoxSide(node.getBBox(), "left"), otherNode);
+ newEdges.set(edgeId, {
+ id: edgeId,
+ fromNode: node.id,
+ fromSide: bestFromSide,
+ fromFloating: true,
+ toNode: otherNode.id,
+ toSide: bestToSide,
+ toFloating: true
+ });
+ }
+ return newEdges;
+ }
+};
+
+// src/canvas-extensions/flip-edge-canvas-extension.ts
+var FlipEdgeCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "flipEdgeFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ }
+ onPopupMenuCreated(canvas) {
+ var _a, _b;
+ const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl;
+ if (!popupMenuEl) return;
+ const POSSIBLE_ICONS = ["lucide-arrow-right", "lucide-move-horizontal", "line-horizontal"];
+ let edgeDirectionButton = null;
+ for (const icon of POSSIBLE_ICONS) {
+ edgeDirectionButton = (_b = popupMenuEl.querySelector(`button:not([id]) > .svg-icon.${icon}`)) == null ? void 0 : _b.parentElement;
+ if (edgeDirectionButton) break;
+ }
+ if (!edgeDirectionButton) return;
+ edgeDirectionButton.addEventListener("click", () => this.onEdgeDirectionDropdownCreated(canvas));
+ }
+ onEdgeDirectionDropdownCreated(canvas) {
+ const dropdownEl = document.body.querySelector("div.menu");
+ if (!dropdownEl) return;
+ const separatorEl = CanvasHelper.createDropdownSeparatorElement();
+ dropdownEl.appendChild(separatorEl);
+ const flipEdgeButton = CanvasHelper.createDropdownOptionElement({
+ icon: "flip-horizontal-2",
+ label: "Flip Edge",
+ callback: () => this.flipEdge(canvas)
+ });
+ dropdownEl.appendChild(flipEdgeButton);
+ }
+ flipEdge(canvas) {
+ const selectedEdges = [...canvas.selection].filter((item) => item.path !== void 0);
+ if (selectedEdges.length === 0) return;
+ for (const edge of selectedEdges) {
+ edge.update({
+ ...edge.from,
+ node: edge.to.node,
+ side: edge.to.side
+ }, {
+ ...edge.to,
+ node: edge.from.node,
+ side: edge.from.side
+ });
+ }
+ canvas.pushHistory(canvas.getData());
+ }
+};
+
+// src/canvas-extensions/edge-selection-canvas-extension.ts
+var DIRECTION_MENU_MAP = {
+ connected: {
+ id: "select-connected-edges",
+ icon: "arrows-selected",
+ label: "Select Connected Edges"
+ },
+ outgoing: {
+ id: "select-outgoing-edges",
+ icon: "arrow-right-selected",
+ label: "Select Outgoing Edges"
+ },
+ incoming: {
+ id: "select-incoming-edges",
+ icon: "arrow-left-selected",
+ label: "Select Incoming Edges"
+ }
+};
+var EdgeSelectionCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "edgeSelectionEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:popup-menu-created",
+ (canvas) => this.onPopupMenuCreated(canvas)
+ ));
+ }
+ onPopupMenuCreated(canvas) {
+ var _a;
+ const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl;
+ if (!popupMenuEl) return;
+ const selectionNodeData = canvas.getSelectionData().nodes;
+ if (canvas.readonly || selectionNodeData.length === 0) return;
+ const selectEdgeByDirection = this.plugin.settings.getSetting("selectEdgeByDirection");
+ const menuDirectionSet = /* @__PURE__ */ new Set(["connected"]);
+ if (selectionNodeData.length === 1) {
+ const node = canvas.nodes.get(selectionNodeData[0].id);
+ if (!node) return;
+ const edges = canvas.getEdgesForNode(node);
+ if (edges.length === 0) return;
+ if (selectEdgeByDirection) {
+ edges.forEach((edge) => {
+ if (edge.from.node === node) {
+ menuDirectionSet.add("outgoing");
+ } else if (edge.to.node === node) {
+ menuDirectionSet.add("incoming");
+ }
+ });
+ }
+ } else if (selectEdgeByDirection) {
+ menuDirectionSet.add("outgoing");
+ menuDirectionSet.add("incoming");
+ }
+ menuDirectionSet.forEach((direction) => {
+ const config = DIRECTION_MENU_MAP[direction];
+ CanvasHelper.addPopupMenuOption(canvas, CanvasHelper.createPopupMenuOption({
+ ...config,
+ callback: () => CanvasHelper.selectEdgesForNodes(canvas, direction)
+ }));
+ });
+ }
+};
+
+// node_modules/html-to-image/es/util.js
+function resolveUrl(url, baseUrl) {
+ if (url.match(/^[a-z]+:\/\//i)) {
+ return url;
+ }
+ if (url.match(/^\/\//)) {
+ return window.location.protocol + url;
+ }
+ if (url.match(/^[a-z]+:/i)) {
+ return url;
+ }
+ const doc = document.implementation.createHTMLDocument();
+ const base = doc.createElement("base");
+ const a = doc.createElement("a");
+ doc.head.appendChild(base);
+ doc.body.appendChild(a);
+ if (baseUrl) {
+ base.href = baseUrl;
+ }
+ a.href = url;
+ return a.href;
+}
+var uuid = /* @__PURE__ */ (() => {
+ let counter = 0;
+ const random = () => (
+ // eslint-disable-next-line no-bitwise
+ `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4)
+ );
+ return () => {
+ counter += 1;
+ return `u${random()}${counter}`;
+ };
+})();
+function toArray(arrayLike) {
+ const arr = [];
+ for (let i = 0, l = arrayLike.length; i < l; i++) {
+ arr.push(arrayLike[i]);
+ }
+ return arr;
+}
+function px(node, styleProperty) {
+ const win = node.ownerDocument.defaultView || window;
+ const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
+ return val ? parseFloat(val.replace("px", "")) : 0;
+}
+function getNodeWidth(node) {
+ const leftBorder = px(node, "border-left-width");
+ const rightBorder = px(node, "border-right-width");
+ return node.clientWidth + leftBorder + rightBorder;
+}
+function getNodeHeight(node) {
+ const topBorder = px(node, "border-top-width");
+ const bottomBorder = px(node, "border-bottom-width");
+ return node.clientHeight + topBorder + bottomBorder;
+}
+function getImageSize(targetNode, options = {}) {
+ const width = options.width || getNodeWidth(targetNode);
+ const height = options.height || getNodeHeight(targetNode);
+ return { width, height };
+}
+function getPixelRatio() {
+ let ratio;
+ let FINAL_PROCESS;
+ try {
+ FINAL_PROCESS = process;
+ } catch (e) {
+ }
+ const val = FINAL_PROCESS && FINAL_PROCESS.env ? FINAL_PROCESS.env.devicePixelRatio : null;
+ if (val) {
+ ratio = parseInt(val, 10);
+ if (Number.isNaN(ratio)) {
+ ratio = 1;
+ }
+ }
+ return ratio || window.devicePixelRatio || 1;
+}
+var canvasDimensionLimit = 16384;
+function checkCanvasDimensions(canvas) {
+ if (canvas.width > canvasDimensionLimit || canvas.height > canvasDimensionLimit) {
+ if (canvas.width > canvasDimensionLimit && canvas.height > canvasDimensionLimit) {
+ if (canvas.width > canvas.height) {
+ canvas.height *= canvasDimensionLimit / canvas.width;
+ canvas.width = canvasDimensionLimit;
+ } else {
+ canvas.width *= canvasDimensionLimit / canvas.height;
+ canvas.height = canvasDimensionLimit;
+ }
+ } else if (canvas.width > canvasDimensionLimit) {
+ canvas.height *= canvasDimensionLimit / canvas.width;
+ canvas.width = canvasDimensionLimit;
+ } else {
+ canvas.width *= canvasDimensionLimit / canvas.height;
+ canvas.height = canvasDimensionLimit;
+ }
+ }
+}
+function createImage(url) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.decode = () => resolve(img);
+ img.onload = () => resolve(img);
+ img.onerror = reject;
+ img.crossOrigin = "anonymous";
+ img.decoding = "async";
+ img.src = url;
+ });
+}
+async function svgToDataURL(svg) {
+ return Promise.resolve().then(() => new XMLSerializer().serializeToString(svg)).then(encodeURIComponent).then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
+}
+async function nodeToDataURL(node, width, height) {
+ const xmlns = "http://www.w3.org/2000/svg";
+ const svg = document.createElementNS(xmlns, "svg");
+ const foreignObject = document.createElementNS(xmlns, "foreignObject");
+ svg.setAttribute("width", `${width}`);
+ svg.setAttribute("height", `${height}`);
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ foreignObject.setAttribute("width", "100%");
+ foreignObject.setAttribute("height", "100%");
+ foreignObject.setAttribute("x", "0");
+ foreignObject.setAttribute("y", "0");
+ foreignObject.setAttribute("externalResourcesRequired", "true");
+ svg.appendChild(foreignObject);
+ foreignObject.appendChild(node);
+ return svgToDataURL(svg);
+}
+var isInstanceOfElement = (node, instance) => {
+ if (node instanceof instance)
+ return true;
+ const nodePrototype = Object.getPrototypeOf(node);
+ if (nodePrototype === null)
+ return false;
+ return nodePrototype.constructor.name === instance.name || isInstanceOfElement(nodePrototype, instance);
+};
+
+// node_modules/html-to-image/es/clone-pseudos.js
+function formatCSSText(style) {
+ const content = style.getPropertyValue("content");
+ return `${style.cssText} content: '${content.replace(/'|"/g, "")}';`;
+}
+function formatCSSProperties(style) {
+ return toArray(style).map((name) => {
+ const value = style.getPropertyValue(name);
+ const priority = style.getPropertyPriority(name);
+ return `${name}: ${value}${priority ? " !important" : ""};`;
+ }).join(" ");
+}
+function getPseudoElementStyle(className, pseudo, style) {
+ const selector = `.${className}:${pseudo}`;
+ const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style);
+ return document.createTextNode(`${selector}{${cssText}}`);
+}
+function clonePseudoElement(nativeNode, clonedNode, pseudo) {
+ const style = window.getComputedStyle(nativeNode, pseudo);
+ const content = style.getPropertyValue("content");
+ if (content === "" || content === "none") {
+ return;
+ }
+ const className = uuid();
+ try {
+ clonedNode.className = `${clonedNode.className} ${className}`;
+ } catch (err) {
+ return;
+ }
+ const styleElement = document.createElement("style");
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style));
+ clonedNode.appendChild(styleElement);
+}
+function clonePseudoElements(nativeNode, clonedNode) {
+ clonePseudoElement(nativeNode, clonedNode, ":before");
+ clonePseudoElement(nativeNode, clonedNode, ":after");
+}
+
+// node_modules/html-to-image/es/mimes.js
+var WOFF = "application/font-woff";
+var JPEG = "image/jpeg";
+var mimes = {
+ woff: WOFF,
+ woff2: WOFF,
+ ttf: "application/font-truetype",
+ eot: "application/vnd.ms-fontobject",
+ png: "image/png",
+ jpg: JPEG,
+ jpeg: JPEG,
+ gif: "image/gif",
+ tiff: "image/tiff",
+ svg: "image/svg+xml",
+ webp: "image/webp"
+};
+function getExtension(url) {
+ const match = /\.([^./]*?)$/g.exec(url);
+ return match ? match[1] : "";
+}
+function getMimeType(url) {
+ const extension = getExtension(url).toLowerCase();
+ return mimes[extension] || "";
+}
+
+// node_modules/html-to-image/es/dataurl.js
+function getContentFromDataUrl(dataURL) {
+ return dataURL.split(/,/)[1];
+}
+function isDataUrl(url) {
+ return url.search(/^(data:)/) !== -1;
+}
+function makeDataUrl(content, mimeType) {
+ return `data:${mimeType};base64,${content}`;
+}
+async function fetchAsDataURL(url, init, process2) {
+ const res = await fetch(url, init);
+ if (res.status === 404) {
+ throw new Error(`Resource "${res.url}" not found`);
+ }
+ const blob = await res.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onloadend = () => {
+ try {
+ resolve(process2({ res, result: reader.result }));
+ } catch (error) {
+ reject(error);
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+var cache = {};
+function getCacheKey(url, contentType, includeQueryParams) {
+ let key = url.replace(/\?.*/, "");
+ if (includeQueryParams) {
+ key = url;
+ }
+ if (/ttf|otf|eot|woff2?/i.test(key)) {
+ key = key.replace(/.*\//, "");
+ }
+ return contentType ? `[${contentType}]${key}` : key;
+}
+async function resourceToDataURL(resourceUrl, contentType, options) {
+ const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
+ if (cache[cacheKey] != null) {
+ return cache[cacheKey];
+ }
+ if (options.cacheBust) {
+ resourceUrl += (/\?/.test(resourceUrl) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime();
+ }
+ let dataURL;
+ try {
+ const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
+ if (!contentType) {
+ contentType = res.headers.get("Content-Type") || "";
+ }
+ return getContentFromDataUrl(result);
+ });
+ dataURL = makeDataUrl(content, contentType);
+ } catch (error) {
+ dataURL = options.imagePlaceholder || "";
+ let msg = `Failed to fetch resource: ${resourceUrl}`;
+ if (error) {
+ msg = typeof error === "string" ? error : error.message;
+ }
+ if (msg) {
+ console.warn(msg);
+ }
+ }
+ cache[cacheKey] = dataURL;
+ return dataURL;
+}
+
+// node_modules/html-to-image/es/clone-node.js
+async function cloneCanvasElement(canvas) {
+ const dataURL = canvas.toDataURL();
+ if (dataURL === "data:,") {
+ return canvas.cloneNode(false);
+ }
+ return createImage(dataURL);
+}
+async function cloneVideoElement(video, options) {
+ if (video.currentSrc) {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ canvas.width = video.clientWidth;
+ canvas.height = video.clientHeight;
+ ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+ const dataURL2 = canvas.toDataURL();
+ return createImage(dataURL2);
+ }
+ const poster = video.poster;
+ const contentType = getMimeType(poster);
+ const dataURL = await resourceToDataURL(poster, contentType, options);
+ return createImage(dataURL);
+}
+async function cloneIFrameElement(iframe) {
+ var _a;
+ try {
+ if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
+ return await cloneNode(iframe.contentDocument.body, {}, true);
+ }
+ } catch (_b) {
+ }
+ return iframe.cloneNode(false);
+}
+async function cloneSingleNode(node, options) {
+ if (isInstanceOfElement(node, HTMLCanvasElement)) {
+ return cloneCanvasElement(node);
+ }
+ if (isInstanceOfElement(node, HTMLVideoElement)) {
+ return cloneVideoElement(node, options);
+ }
+ if (isInstanceOfElement(node, HTMLIFrameElement)) {
+ return cloneIFrameElement(node);
+ }
+ return node.cloneNode(false);
+}
+var isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SLOT";
+async function cloneChildren(nativeNode, clonedNode, options) {
+ var _a, _b;
+ let children = [];
+ if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
+ children = toArray(nativeNode.assignedNodes());
+ } else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) {
+ children = toArray(nativeNode.contentDocument.body.childNodes);
+ } else {
+ children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes);
+ }
+ if (children.length === 0 || isInstanceOfElement(nativeNode, HTMLVideoElement)) {
+ return clonedNode;
+ }
+ await children.reduce((deferred, child) => deferred.then(() => cloneNode(child, options)).then((clonedChild) => {
+ if (clonedChild) {
+ clonedNode.appendChild(clonedChild);
+ }
+ }), Promise.resolve());
+ return clonedNode;
+}
+function cloneCSSStyle(nativeNode, clonedNode) {
+ const targetStyle = clonedNode.style;
+ if (!targetStyle) {
+ return;
+ }
+ const sourceStyle = window.getComputedStyle(nativeNode);
+ if (sourceStyle.cssText) {
+ targetStyle.cssText = sourceStyle.cssText;
+ targetStyle.transformOrigin = sourceStyle.transformOrigin;
+ } else {
+ toArray(sourceStyle).forEach((name) => {
+ let value = sourceStyle.getPropertyValue(name);
+ if (name === "font-size" && value.endsWith("px")) {
+ const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1;
+ value = `${reducedFont}px`;
+ }
+ if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && name === "display" && value === "inline") {
+ value = "block";
+ }
+ if (name === "d" && clonedNode.getAttribute("d")) {
+ value = `path(${clonedNode.getAttribute("d")})`;
+ }
+ targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name));
+ });
+ }
+}
+function cloneInputValue(nativeNode, clonedNode) {
+ if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) {
+ clonedNode.innerHTML = nativeNode.value;
+ }
+ if (isInstanceOfElement(nativeNode, HTMLInputElement)) {
+ clonedNode.setAttribute("value", nativeNode.value);
+ }
+}
+function cloneSelectValue(nativeNode, clonedNode) {
+ if (isInstanceOfElement(nativeNode, HTMLSelectElement)) {
+ const clonedSelect = clonedNode;
+ const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute("value"));
+ if (selectedOption) {
+ selectedOption.setAttribute("selected", "");
+ }
+ }
+}
+function decorate(nativeNode, clonedNode) {
+ if (isInstanceOfElement(clonedNode, Element)) {
+ cloneCSSStyle(nativeNode, clonedNode);
+ clonePseudoElements(nativeNode, clonedNode);
+ cloneInputValue(nativeNode, clonedNode);
+ cloneSelectValue(nativeNode, clonedNode);
+ }
+ return clonedNode;
+}
+async function ensureSVGSymbols(clone, options) {
+ const uses = clone.querySelectorAll ? clone.querySelectorAll("use") : [];
+ if (uses.length === 0) {
+ return clone;
+ }
+ const processedDefs = {};
+ for (let i = 0; i < uses.length; i++) {
+ const use = uses[i];
+ const id = use.getAttribute("xlink:href");
+ if (id) {
+ const exist = clone.querySelector(id);
+ const definition = document.querySelector(id);
+ if (!exist && definition && !processedDefs[id]) {
+ processedDefs[id] = await cloneNode(definition, options, true);
+ }
+ }
+ }
+ const nodes = Object.values(processedDefs);
+ if (nodes.length) {
+ const ns = "http://www.w3.org/1999/xhtml";
+ const svg = document.createElementNS(ns, "svg");
+ svg.setAttribute("xmlns", ns);
+ svg.style.position = "absolute";
+ svg.style.width = "0";
+ svg.style.height = "0";
+ svg.style.overflow = "hidden";
+ svg.style.display = "none";
+ const defs = document.createElementNS(ns, "defs");
+ svg.appendChild(defs);
+ for (let i = 0; i < nodes.length; i++) {
+ defs.appendChild(nodes[i]);
+ }
+ clone.appendChild(svg);
+ }
+ return clone;
+}
+async function cloneNode(node, options, isRoot) {
+ if (!isRoot && options.filter && !options.filter(node)) {
+ return null;
+ }
+ return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode)).then((clonedNode) => ensureSVGSymbols(clonedNode, options));
+}
+
+// node_modules/html-to-image/es/embed-resources.js
+var URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g;
+var URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
+var FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
+function toRegex(url) {
+ const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
+ return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g");
+}
+function parseURLs(cssText) {
+ const urls = [];
+ cssText.replace(URL_REGEX, (raw, quotation, url) => {
+ urls.push(url);
+ return raw;
+ });
+ return urls.filter((url) => !isDataUrl(url));
+}
+async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) {
+ try {
+ const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL;
+ const contentType = getMimeType(resourceURL);
+ let dataURL;
+ if (getContentFromUrl) {
+ const content = await getContentFromUrl(resolvedURL);
+ dataURL = makeDataUrl(content, contentType);
+ } else {
+ dataURL = await resourceToDataURL(resolvedURL, contentType, options);
+ }
+ return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`);
+ } catch (error) {
+ }
+ return cssText;
+}
+function filterPreferredFontFormat(str, { preferredFontFormat }) {
+ return !preferredFontFormat ? str : str.replace(FONT_SRC_REGEX, (match) => {
+ while (true) {
+ const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || [];
+ if (!format) {
+ return "";
+ }
+ if (format === preferredFontFormat) {
+ return `src: ${src};`;
+ }
+ }
+ });
+}
+function shouldEmbed(url) {
+ return url.search(URL_REGEX) !== -1;
+}
+async function embedResources(cssText, baseUrl, options) {
+ if (!shouldEmbed(cssText)) {
+ return cssText;
+ }
+ const filteredCSSText = filterPreferredFontFormat(cssText, options);
+ const urls = parseURLs(filteredCSSText);
+ return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText));
+}
+
+// node_modules/html-to-image/es/embed-images.js
+async function embedProp(propName, node, options) {
+ var _a;
+ const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName);
+ if (propValue) {
+ const cssString = await embedResources(propValue, null, options);
+ node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName));
+ return true;
+ }
+ return false;
+}
+async function embedBackground(clonedNode, options) {
+ if (!await embedProp("background", clonedNode, options)) {
+ await embedProp("background-image", clonedNode, options);
+ }
+ if (!await embedProp("mask", clonedNode, options)) {
+ await embedProp("mask-image", clonedNode, options);
+ }
+}
+async function embedImageNode(clonedNode, options) {
+ const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);
+ if (!(isImageElement && !isDataUrl(clonedNode.src)) && !(isInstanceOfElement(clonedNode, SVGImageElement) && !isDataUrl(clonedNode.href.baseVal))) {
+ return;
+ }
+ const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal;
+ const dataURL = await resourceToDataURL(url, getMimeType(url), options);
+ await new Promise((resolve, reject) => {
+ clonedNode.onload = resolve;
+ clonedNode.onerror = reject;
+ const image = clonedNode;
+ if (image.decode) {
+ image.decode = resolve;
+ }
+ if (image.loading === "lazy") {
+ image.loading = "eager";
+ }
+ if (isImageElement) {
+ clonedNode.srcset = "";
+ clonedNode.src = dataURL;
+ } else {
+ clonedNode.href.baseVal = dataURL;
+ }
+ });
+}
+async function embedChildren(clonedNode, options) {
+ const children = toArray(clonedNode.childNodes);
+ const deferreds = children.map((child) => embedImages(child, options));
+ await Promise.all(deferreds).then(() => clonedNode);
+}
+async function embedImages(clonedNode, options) {
+ if (isInstanceOfElement(clonedNode, Element)) {
+ await embedBackground(clonedNode, options);
+ await embedImageNode(clonedNode, options);
+ await embedChildren(clonedNode, options);
+ }
+}
+
+// node_modules/html-to-image/es/apply-style.js
+function applyStyle(node, options) {
+ const { style } = node;
+ if (options.backgroundColor) {
+ style.backgroundColor = options.backgroundColor;
+ }
+ if (options.width) {
+ style.width = `${options.width}px`;
+ }
+ if (options.height) {
+ style.height = `${options.height}px`;
+ }
+ const manual = options.style;
+ if (manual != null) {
+ Object.keys(manual).forEach((key) => {
+ style[key] = manual[key];
+ });
+ }
+ return node;
+}
+
+// node_modules/html-to-image/es/embed-webfonts.js
+var cssFetchCache = {};
+async function fetchCSS(url) {
+ let cache2 = cssFetchCache[url];
+ if (cache2 != null) {
+ return cache2;
+ }
+ const res = await fetch(url);
+ const cssText = await res.text();
+ cache2 = { url, cssText };
+ cssFetchCache[url] = cache2;
+ return cache2;
+}
+async function embedFonts(data, options) {
+ let cssText = data.cssText;
+ const regexUrl = /url\(["']?([^"')]+)["']?\)/g;
+ const fontLocs = cssText.match(/url\([^)]+\)/g) || [];
+ const loadFonts = fontLocs.map(async (loc) => {
+ let url = loc.replace(regexUrl, "$1");
+ if (!url.startsWith("https://")) {
+ url = new URL(url, data.url).href;
+ }
+ return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {
+ cssText = cssText.replace(loc, `url(${result})`);
+ return [loc, result];
+ });
+ });
+ return Promise.all(loadFonts).then(() => cssText);
+}
+function parseCSS(source) {
+ if (source == null) {
+ return [];
+ }
+ const result = [];
+ const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi;
+ let cssText = source.replace(commentsRegex, "");
+ const keyframesRegex = new RegExp("((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})", "gi");
+ while (true) {
+ const matches = keyframesRegex.exec(cssText);
+ if (matches === null) {
+ break;
+ }
+ result.push(matches[0]);
+ }
+ cssText = cssText.replace(keyframesRegex, "");
+ const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
+ const combinedCSSRegex = "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})";
+ const unifiedRegex = new RegExp(combinedCSSRegex, "gi");
+ while (true) {
+ let matches = importRegex.exec(cssText);
+ if (matches === null) {
+ matches = unifiedRegex.exec(cssText);
+ if (matches === null) {
+ break;
+ } else {
+ importRegex.lastIndex = unifiedRegex.lastIndex;
+ }
+ } else {
+ unifiedRegex.lastIndex = importRegex.lastIndex;
+ }
+ result.push(matches[0]);
+ }
+ return result;
+}
+async function getCSSRules(styleSheets, options) {
+ const ret = [];
+ const deferreds = [];
+ styleSheets.forEach((sheet) => {
+ if ("cssRules" in sheet) {
+ try {
+ toArray(sheet.cssRules || []).forEach((item, index) => {
+ if (item.type === CSSRule.IMPORT_RULE) {
+ let importIndex = index + 1;
+ const url = item.href;
+ const deferred = fetchCSS(url).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
+ try {
+ sheet.insertRule(rule, rule.startsWith("@import") ? importIndex += 1 : sheet.cssRules.length);
+ } catch (error) {
+ console.error("Error inserting rule from remote css", {
+ rule,
+ error
+ });
+ }
+ })).catch((e) => {
+ console.error("Error loading remote css", e.toString());
+ });
+ deferreds.push(deferred);
+ }
+ });
+ } catch (e) {
+ const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
+ if (sheet.href != null) {
+ deferreds.push(fetchCSS(sheet.href).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
+ inline.insertRule(rule, sheet.cssRules.length);
+ })).catch((err) => {
+ console.error("Error loading remote stylesheet", err);
+ }));
+ }
+ console.error("Error inlining remote css file", e);
+ }
+ }
+ });
+ return Promise.all(deferreds).then(() => {
+ styleSheets.forEach((sheet) => {
+ if ("cssRules" in sheet) {
+ try {
+ toArray(sheet.cssRules || []).forEach((item) => {
+ ret.push(item);
+ });
+ } catch (e) {
+ console.error(`Error while reading CSS rules from ${sheet.href}`, e);
+ }
+ }
+ });
+ return ret;
+ });
+}
+function getWebFontRules(cssRules) {
+ return cssRules.filter((rule) => rule.type === CSSRule.FONT_FACE_RULE).filter((rule) => shouldEmbed(rule.style.getPropertyValue("src")));
+}
+async function parseWebFontRules(node, options) {
+ if (node.ownerDocument == null) {
+ throw new Error("Provided element is not within a Document");
+ }
+ const styleSheets = toArray(node.ownerDocument.styleSheets);
+ const cssRules = await getCSSRules(styleSheets, options);
+ return getWebFontRules(cssRules);
+}
+async function getWebFontCSS(node, options) {
+ const rules = await parseWebFontRules(node, options);
+ const cssTexts = await Promise.all(rules.map((rule) => {
+ const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;
+ return embedResources(rule.cssText, baseUrl, options);
+ }));
+ return cssTexts.join("\n");
+}
+async function embedWebFonts(clonedNode, options) {
+ const cssText = options.fontEmbedCSS != null ? options.fontEmbedCSS : options.skipFonts ? null : await getWebFontCSS(clonedNode, options);
+ if (cssText) {
+ const styleNode = document.createElement("style");
+ const sytleContent = document.createTextNode(cssText);
+ styleNode.appendChild(sytleContent);
+ if (clonedNode.firstChild) {
+ clonedNode.insertBefore(styleNode, clonedNode.firstChild);
+ } else {
+ clonedNode.appendChild(styleNode);
+ }
+ }
+}
+
+// node_modules/html-to-image/es/index.js
+async function toSvg(node, options = {}) {
+ const { width, height } = getImageSize(node, options);
+ const clonedNode = await cloneNode(node, options, true);
+ await embedWebFonts(clonedNode, options);
+ await embedImages(clonedNode, options);
+ applyStyle(clonedNode, options);
+ const datauri = await nodeToDataURL(clonedNode, width, height);
+ return datauri;
+}
+async function toCanvas(node, options = {}) {
+ const { width, height } = getImageSize(node, options);
+ const svg = await toSvg(node, options);
+ const img = await createImage(svg);
+ const canvas = document.createElement("canvas");
+ const context = canvas.getContext("2d");
+ const ratio = options.pixelRatio || getPixelRatio();
+ const canvasWidth = options.canvasWidth || width;
+ const canvasHeight = options.canvasHeight || height;
+ canvas.width = canvasWidth * ratio;
+ canvas.height = canvasHeight * ratio;
+ if (!options.skipAutoScale) {
+ checkCanvasDimensions(canvas);
+ }
+ canvas.style.width = `${canvasWidth}`;
+ canvas.style.height = `${canvasHeight}`;
+ if (options.backgroundColor) {
+ context.fillStyle = options.backgroundColor;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ }
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
+ return canvas;
+}
+async function toPng(node, options = {}) {
+ const canvas = await toCanvas(node, options);
+ return canvas.toDataURL();
+}
+
+// src/canvas-extensions/export-canvas-extension.ts
+var import_obsidian18 = require("obsidian");
+var MAX_ALLOWED_LOADING_TIME = 1e4;
+var ExportCanvasExtension = class extends CanvasExtension {
+ isEnabled() {
+ return "betterExportFeatureEnabled";
+ }
+ init() {
+ this.plugin.registerEvent(this.plugin.app.workspace.on(
+ "advanced-canvas:node-breakpoint-changed",
+ (canvas, node, breakpointRef) => {
+ if (canvas.screenshotting) breakpointRef.value = true;
+ }
+ ));
+ this.plugin.addCommand({
+ id: "export-all-as-image",
+ name: "Export canvas as image",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.nodes.size > 0,
+ (canvas) => this.showExportImageSettingsModal(canvas, null)
+ )
+ });
+ this.plugin.addCommand({
+ id: "export-selected-as-image",
+ name: "Export selected nodes as image",
+ checkCallback: CanvasHelper.canvasCommand(
+ this.plugin,
+ (canvas) => canvas.selection.size > 0,
+ (canvas) => this.showExportImageSettingsModal(
+ canvas,
+ canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0)
+ )
+ )
+ });
+ }
+ async showExportImageSettingsModal(canvas, nodesToExport) {
+ const modal = new import_obsidian18.Modal(this.plugin.app);
+ modal.setTitle("Export image settings");
+ let pixelRatioSetting = null;
+ let noFontExportSetting = null;
+ let transparentBackgroundSetting = null;
+ const updateDynamicSettings = () => {
+ var _a, _b, _c, _d, _e, _f;
+ if (svg) {
+ (_a = pixelRatioSetting == null ? void 0 : pixelRatioSetting.settingEl) == null ? void 0 : _a.hide();
+ (_b = noFontExportSetting == null ? void 0 : noFontExportSetting.settingEl) == null ? void 0 : _b.show();
+ (_c = transparentBackgroundSetting == null ? void 0 : transparentBackgroundSetting.settingEl) == null ? void 0 : _c.hide();
+ } else {
+ (_d = pixelRatioSetting == null ? void 0 : pixelRatioSetting.settingEl) == null ? void 0 : _d.show();
+ (_e = noFontExportSetting == null ? void 0 : noFontExportSetting.settingEl) == null ? void 0 : _e.hide();
+ (_f = transparentBackgroundSetting == null ? void 0 : transparentBackgroundSetting.settingEl) == null ? void 0 : _f.show();
+ }
+ };
+ let svg = false;
+ new import_obsidian18.Setting(modal.contentEl).setName("Export file format").setDesc("Choose the file format to export the canvas as.").addDropdown(
+ (dropdown) => dropdown.addOptions({
+ png: "PNG",
+ svg: "SVG"
+ }).setValue(svg ? "svg" : "png").onChange((value) => {
+ svg = value === "svg";
+ updateDynamicSettings();
+ })
+ );
+ let pixelRatioFactor = 1;
+ pixelRatioSetting = new import_obsidian18.Setting(modal.contentEl).setName("Pixel ratio").setDesc("Higher pixel ratios result in higher resolution images but also larger file sizes.").addSlider(
+ (slider) => slider.setDynamicTooltip().setLimits(0.2, 5, 0.1).setValue(pixelRatioFactor).onChange((value) => pixelRatioFactor = value)
+ );
+ let noFontExport = true;
+ noFontExportSetting = new import_obsidian18.Setting(modal.contentEl).setName("Skip font export").setDesc("This will not include the fonts in the exported SVG. This will make the SVG file smaller.").addToggle(
+ (toggle) => toggle.setValue(noFontExport).onChange((value) => noFontExport = value)
+ );
+ let theme = document.body.classList.contains("theme-dark") ? "dark" : "light";
+ new import_obsidian18.Setting(modal.contentEl).setName("Theme").setDesc("The theme used for the export.").addDropdown(
+ (dropdown) => dropdown.addOptions({
+ light: "Light",
+ dark: "Dark"
+ }).setValue(theme).onChange((value) => theme = value)
+ );
+ let watermark = false;
+ new import_obsidian18.Setting(modal.contentEl).setName("Show logo").setDesc("This will add an Obsidian + Advanced Canvas logo to the bottom left.").addToggle(
+ (toggle) => toggle.setValue(watermark).onChange((value) => watermark = value)
+ );
+ let garbledText = false;
+ new import_obsidian18.Setting(modal.contentEl).setName("Privacy mode").setDesc("This will obscure any text on your canvas.").addToggle(
+ (toggle) => toggle.setValue(garbledText).onChange((value) => garbledText = value)
+ );
+ let transparentBackground = false;
+ transparentBackgroundSetting = new import_obsidian18.Setting(modal.contentEl).setName("Transparent background").setDesc("This will make the background of the image transparent.").addToggle(
+ (toggle) => toggle.setValue(transparentBackground).onChange((value) => transparentBackground = value)
+ );
+ new import_obsidian18.Setting(modal.contentEl).addButton(
+ (button) => button.setButtonText("Save").setCta().onClick(async () => {
+ modal.close();
+ this.exportImage(
+ canvas,
+ nodesToExport,
+ svg,
+ svg ? 1 : pixelRatioFactor,
+ svg ? noFontExport : false,
+ theme,
+ watermark,
+ garbledText,
+ svg ? true : transparentBackground
+ );
+ })
+ );
+ updateDynamicSettings();
+ modal.open();
+ }
+ async exportImage(canvas, nodesToExport, svg, pixelRatioFactor, noFontExport, theme, watermark, garbledText, transparentBackground) {
+ var _a, _b, _c;
+ const cachedTheme = document.body.classList.contains("theme-dark") ? "dark" : "light";
+ if (theme !== cachedTheme) {
+ document.body.classList.toggle("theme-dark", theme === "dark");
+ document.body.classList.toggle("theme-light", theme === "light");
+ }
+ const isWholeCanvas = nodesToExport === null;
+ if (!nodesToExport) nodesToExport = [...canvas.nodes.values()];
+ const nodesToExportIds = nodesToExport.map((node) => node.getData().id);
+ const edgesToExport = [...canvas.edges.values()].filter((edge) => {
+ const edgeData = edge.getData();
+ return nodesToExportIds.includes(edgeData.fromNode) && nodesToExportIds.includes(edgeData.toNode);
+ });
+ const backgroundColor = transparentBackground ? void 0 : window.getComputedStyle(canvas.canvasEl).getPropertyValue("--canvas-background");
+ new import_obsidian18.Notice("Exporting the canvas. Please wait...");
+ const interactionBlocker = this.getInteractionBlocker();
+ document.body.appendChild(interactionBlocker);
+ canvas.screenshotting = true;
+ canvas.canvasEl.classList.add("is-exporting");
+ if (garbledText) canvas.canvasEl.classList.add("is-text-garbled");
+ let watermarkEl = null;
+ const cachedSelection = new Set(canvas.selection);
+ canvas.deselectAll();
+ const cachedViewport = { x: canvas.x, y: canvas.y, zoom: canvas.zoom };
+ try {
+ const targetBoundingBox = CanvasHelper.getBBox([...nodesToExport, ...edgesToExport]);
+ let enlargedTargetBoundingBox = BBoxHelper.scaleBBox(targetBoundingBox, 1.1);
+ const enlargedTargetBoundingBoxSize = { width: enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX, height: enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY };
+ const canvasElSize = { width: canvas.canvasEl.clientWidth, height: canvas.canvasEl.clientHeight };
+ const requiredPixelRatio = Math.max(enlargedTargetBoundingBoxSize.width / canvasElSize.width, enlargedTargetBoundingBoxSize.height / canvasElSize.height);
+ const pixelRatio = svg ? void 0 : Math.round(requiredPixelRatio * pixelRatioFactor);
+ watermarkEl = watermark ? this.getWatermark(enlargedTargetBoundingBox) : null;
+ if (watermarkEl) canvas.canvasEl.appendChild(watermarkEl);
+ const actualAspectRatio = canvas.canvasRect.width / canvas.canvasRect.height;
+ const targetAspectRatio = (enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX) / (enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY);
+ let adjustedBoundingBox = { ...enlargedTargetBoundingBox };
+ if (actualAspectRatio > targetAspectRatio) {
+ const targetHeight = enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY;
+ const actualWidth = targetHeight * actualAspectRatio;
+ adjustedBoundingBox.maxX = enlargedTargetBoundingBox.minX + actualWidth;
+ } else {
+ const targetWidth = enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX;
+ const actualHeight = targetWidth / actualAspectRatio;
+ adjustedBoundingBox.maxY = enlargedTargetBoundingBox.minY + actualHeight;
+ }
+ canvas.zoomToRealBbox(adjustedBoundingBox);
+ canvas.setViewport(canvas.tx, canvas.ty, canvas.tZoom);
+ await sleep(10);
+ let canvasScale = parseFloat(((_a = canvas.canvasEl.style.transform.match(/scale\((\d+(\.\d+)?)\)/)) == null ? void 0 : _a[1]) || "1");
+ const edgePathsBBox = BBoxHelper.combineBBoxes(edgesToExport.map((edge) => {
+ const edgeCenter = edge.getCenter();
+ const labelWidth = edge.labelElement ? edge.labelElement.wrapperEl.getBoundingClientRect().width / canvasScale : 0;
+ return { minX: edgeCenter.x - labelWidth / 2, minY: edgeCenter.y, maxX: edgeCenter.x + labelWidth / 2, maxY: edgeCenter.y };
+ }));
+ const enlargedEdgePathsBBox = BBoxHelper.enlargeBBox(edgePathsBBox, 1.1);
+ enlargedTargetBoundingBox = BBoxHelper.combineBBoxes([enlargedTargetBoundingBox, enlargedEdgePathsBBox]);
+ adjustedBoundingBox = BBoxHelper.combineBBoxes([adjustedBoundingBox, enlargedEdgePathsBBox]);
+ canvas.zoomToRealBbox(adjustedBoundingBox);
+ canvas.setViewport(canvas.tx, canvas.ty, canvas.tZoom);
+ await sleep(10);
+ const canvasViewportBBox = canvas.getViewportBBox();
+ canvasScale = parseFloat(((_b = canvas.canvasEl.style.transform.match(/scale\((\d+(\.\d+)?)\)/)) == null ? void 0 : _b[1]) || "1");
+ let width = (canvasViewportBBox.maxX - canvasViewportBBox.minX) * canvasScale;
+ let height = (canvasViewportBBox.maxY - canvasViewportBBox.minY) * canvasScale;
+ if (actualAspectRatio > targetAspectRatio)
+ width = height * targetAspectRatio;
+ else height = width / targetAspectRatio;
+ let unloadedNodes = nodesToExport.filter((node) => node.initialized === false || node.isContentMounted === false);
+ const startTimestamp = performance.now();
+ while (unloadedNodes.length > 0 && performance.now() - startTimestamp < MAX_ALLOWED_LOADING_TIME) {
+ await sleep(10);
+ unloadedNodes = nodesToExport.filter((node) => node.initialized === false || node.isContentMounted === false);
+ console.info(`Waiting for ${unloadedNodes.length} nodes to finish loading...`);
+ }
+ if (unloadedNodes.length === 0) {
+ const nodeElements = nodesToExport.map((node) => node.nodeEl);
+ const edgePathAndArrowElements = edgesToExport.map((edge) => [edge.lineGroupEl, edge.lineEndGroupEl]).flat();
+ const edgeLabelElements = edgesToExport.map((edge) => {
+ var _a2;
+ return (_a2 = edge.labelElement) == null ? void 0 : _a2.wrapperEl;
+ }).filter((labelElement) => labelElement !== void 0);
+ const filter = (element) => {
+ var _a2, _b2, _c2, _d;
+ if (((_a2 = element.classList) == null ? void 0 : _a2.contains("canvas-node")) && !nodeElements.includes(element))
+ return false;
+ if (((_c2 = (_b2 = element.parentElement) == null ? void 0 : _b2.classList) == null ? void 0 : _c2.contains("canvas-edges")) && !edgePathAndArrowElements.includes(element))
+ return false;
+ if (((_d = element.classList) == null ? void 0 : _d.contains("canvas-path-label-wrapper")) && !edgeLabelElements.includes(element))
+ return false;
+ return true;
+ };
+ const options = {
+ pixelRatio,
+ backgroundColor,
+ height,
+ width,
+ filter
+ };
+ if (noFontExport) options.fontEmbedCSS = "";
+ let imageDataUri = svg ? await toSvg(canvas.canvasEl, options) : await toPng(canvas.canvasEl, options);
+ if (svg) {
+ const header = ``;
+ imageDataUri = imageDataUri.replace(
+ encodeURIComponent("