0.18.0-17 - Text to Path

This commit is contained in:
zsviczian
2025-05-25 19:18:00 +02:00
parent cac27fb936
commit dd7abe2547
16 changed files with 1651 additions and 639 deletions

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -1,613 +0,0 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg)
This script allows you to fit a text element along a selected path (line, arrow, freedraw, ellipse, rectangle, or diamond) in Excalidraw. You can select either a path or a text element, or both:
- If only a path is selected, you will be prompted to provide the text.
- If only a text element is selected and it was previously fitted to a path, the script will use the original path if it is still present in the scene.
- If both a text and a path are selected, the script will fit the text to the selected path.
If the path is a perfect circle, you will be prompted to choose whether to fit the text above or below the circle.
After fitting, the text will no longer be editable as a standard text element or function as a markdown link. Emojis are not supported.
```javascript
*/
els = ea.getViewSelectedElements();
let pathEl = els.find(el=>["ellipse", "rectangle", "diamond", "line", "arrow", "freedraw"].includes(el.type));
const textEl = els.find(el=>el.type === "text");
const win = ea.targetView.ownerWindow;
let pathElID = textEl?.customData?.text2Path?.pathElID;
if(!pathEl) {
if (pathElID) {
pathEl = ea.getViewElements().find(el=>el.id === pathElID);
pathElID = pathEl?.id;
}
if(!pathElID) {
new Notice("Please select a text element and a valid path element (ellipse, rectangle, diamond, line, arrow, or freedraw)");
return;
}
} else {
pathElID = pathEl.id;
}
const aspectRatio = pathEl.width/pathEl.height;
const isCircle = pathEl.type === "ellipse" && aspectRatio > 0.9 && aspectRatio < 1.1;
// ---------------------------------------------------------
// Convert path to SVG and use real path for text placement.
// ---------------------------------------------------------
if((["line", "arrow"].includes(pathEl.type) && pathEl.roundness !== null) || ["freedraw", "rectangle", "diamond"].includes(pathEl.type)) {
pathEl = await convertBezierToPoints();
}
// ---------------------------------------------------------
// Retreive original text from text-on-path customData
// ---------------------------------------------------------
const initialOffset = textEl?.customData?.text2Path?.offset ?? 0;
const initialArchAbove = textEl?.customData?.text2Path?.archAbove ?? true;
const text = (await utils.inputPrompt({
header: "Edit",
value: textEl?.customData?.text2Path
? textEl.customData.text2Path.text
: textEl?.text ?? "",
lines: 3,
customComponents: isCircle ? circleArchControl : offsetControl,
})).replace(" \n"," ").replace("\n ", " ").replace("\n"," ");
if(!text) {
new Notice("No text provided!");
return;
}
// -------------------------------------
// Copy font style to ExcalidrawAutomate
// -------------------------------------
const st = ea.getExcalidrawAPI().getAppState();
ea.style.fontSize = textEl?.fontSize ?? st.currentItemFontSize;
ea.style.fontFamily = textEl?.fontFamily ?? st.currentItemFontFamily;
ea.style.strokeColor = textEl?.strokeColor ?? st.currentItemStrokeColor;
ea.style.opacity = textEl?.opacity ?? st.currentItemOpacity;
// -----------------------------------
// Delete previous text arch if exists
// -----------------------------------
if (textEl?.customData?.text2Path) {
const pathID = textEl.customData.text2Path.pathID;
const elements = ea.getViewElements().filter(el=>el.customData?.text2Path && el.customData.text2Path.pathID === pathID);
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach(el=>{el.isDeleted = true;});
} else {
if(textEl) {
ea.copyViewElementsToEAforEditing([textEl]);
ea.getElements().forEach(el=>{el.isDeleted = true;});
}
}
// --------------------------------------------------------
// Use original text arch algorithm in case shape is circle
// --------------------------------------------------------
if (isCircle) {
const r = (pathEl.width+pathEl.height)/4;
const archAbove = win.ArchPosition ?? initialArchAbove;
if (textEl.customData?.text2Path) {
const pathID = textEl.customData.text2Path.pathID;
const elements = ea.getViewElements().filter(el=>el.customData?.text2Path && el.customData.text2Path.pathID === pathID);
ea.copyViewElementsToEAforEditing(elements);
} else {
ea.copyViewElementsToEAforEditing([textEl]);
}
ea.getElements().forEach(el=>{el.isDeleted = true;});
// Define center point of the ellipse
const centerX = pathEl.x + r;
const centerY = pathEl.y + r;
function circlePoint(angle) {
// Calculate point exactly on the ellipse's circumference
return [
centerX + r * Math.sin(angle),
centerY - r * Math.cos(angle)
];
}
// Calculate the text width to center it properly
const textWidth = ea.measureText(text).width;
// Calculate starting angle based on arch position
// For "Arch above", start at top (0 radians)
// For "Arch below", start at bottom (π radians)
const startAngle = archAbove ? 0 : Math.PI;
// Calculate how much of the circle arc the text will occupy
const arcLength = textWidth / r;
// Set the starting rotation to center the text at the top/bottom point
let rot = startAngle - arcLength / 2;
const pathID = ea.generateElementId();
let objectIDs = [];
for(
archAbove ? i=0 : i=text.length-1;
archAbove ? i<text.length : i>=0;
archAbove ? i++ : i--
) {
const character = text.substring(i,i+1);
const charMetrics = ea.measureText(character);
const charWidth = charMetrics.width / r;
// Adjust rotation to position the current character
const charAngle = rot + charWidth / 2;
// Calculate point on the circle's edge
const [baseX, baseY] = circlePoint(charAngle);
// Center each character horizontally and vertically
// Use the actual character width and height for precise placement
const charPixelWidth = charMetrics.width;
const charPixelHeight = charMetrics.height;
// Place the character so its center is on the circle
const x = baseX - charPixelWidth / 2;
const y = baseY - charPixelHeight / 2;
// Set rotation for the character to align with the tangent of the circle
// No additional 90 degree rotation needed
ea.style.angle = charAngle + (archAbove ? 0 : Math.PI);
const charID = ea.addText(x, y, character);
ea.addAppendUpdateCustomData(charID, {
text2Path: {pathID, text, pathElID, archAbove, offset: 0}
});
objectIDs.push(charID);
rot += charWidth;
}
const groupID = ea.addToGroup(objectIDs);
const letterSet = new Set(objectIDs);
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ea.getViewElements().filter(el=>letterSet.has(el.id)));
return;
}
// ------------------------------------------------------------
// Convert any shape type to a series of points along a path
// In practice this only applies to ellipses and streight lines
// ------------------------------------------------------------
const pathPoints = calculatePathPoints(pathEl);
// Calculate character metrics for spacing
const charWidths = [];
const charHeights = [];
let totalTextWidth = 0;
for (let i = 0; i < text.length; i++) {
const character = text.substring(i, i+1);
const charWidth = ea.measureText(character).width;
const charHeight = ea.measureText(character).height;
charWidths.push(charWidth);
charHeights.push(charHeight);
totalTextWidth += charWidth;
}
// Generate a unique ID for this text arch
const pathID = ea.generateElementId();
let objectIDs = [];
// Place text along the path with natural spacing
const offset = isCircle ? 0 : (parseInt(win.TextArchOffset ?? initialOffset) || 0);
distributeTextAlongPath(text, pathPoints, pathID, objectIDs, charWidths, charHeights, ea.measureText("i").width*0.3, offset);
// Add all text characters to a group
const groupID = ea.addToGroup(objectIDs);
const letterSet = new Set(objectIDs);
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ea.getViewElements().filter(el=>letterSet.has(el.id)));
function transposeElements(ids) {
const dims = ea.measureText("M");
ea.getElements().filter(el=>ids.has(el.id)).forEach(el=>{
el.x -= dims.width/2;
el.y -= dims.height/2;
})
}
// Function to create the circle arch position control in the dialog
function circleArchControl(container) {
if (typeof win.ArchPosition === "undefined") {
win.ArchPosition = initialArchAbove;
}
const archContainer = container.createDiv();
archContainer.style.display = "flex";
archContainer.style.alignItems = "center";
archContainer.style.marginBottom = "8px";
const label = archContainer.createEl("label");
label.textContent = "Arch position:";
label.style.marginRight = "10px";
label.style.fontWeight = "bold";
const select = archContainer.createEl("select");
// Add options for above/below
const aboveOption = select.createEl("option");
aboveOption.value = "true";
aboveOption.text = "Above";
const belowOption = select.createEl("option");
belowOption.value = "false";
belowOption.text = "Below";
// Set the default value
select.value = win.ArchPosition ? "true" : "false";
select.addEventListener("change", (e) => {
win.ArchPosition = e.target.value === "true";
});
}
// Function to create the offset input control in the dialog
function offsetControl(container) {
if (!win.TextArchOffset) win.TextArchOffset = initialOffset.toString();
const offsetContainer = container.createDiv();
offsetContainer.style.display = "flex";
offsetContainer.style.alignItems = "center";
offsetContainer.style.marginBottom = "8px";
const label = offsetContainer.createEl("label");
label.textContent = "Offset (px):";
label.style.marginRight = "10px";
label.style.fontWeight = "bold";
const input = offsetContainer.createEl("input");
input.type = "number";
input.value = win.TextArchOffset;
input.placeholder = "0";
input.style.width = "60px";
input.style.padding = "4px";
input.addEventListener("input", (e) => {
const val = e.target.value.trim();
if (val === "" || !isNaN(parseInt(val))) {
win.TextArchOffset = val;
} else {
e.target.value = win.TextArchOffset || "0";
}
});
}
// Function to convert any shape to a series of points along its path
function calculatePathPoints(element) {
// Handle closed shapes by converting them to points along their perimeter
if (["ellipse", "rectangle", "diamond"].includes(element.type)) {
return getClosedShapePoints(element);
}
// Handle lines, arrows, and freedraw paths
const points = [];
// Get absolute coordinates of all points
const absolutePoints = element.points.map(point => [
point[0] + element.x,
point[1] + element.y
]);
// Calculate segment information
let segments = [];
for (let i = 0; i < absolutePoints.length - 1; i++) {
const p0 = absolutePoints[i];
const p1 = absolutePoints[i+1];
const dx = p1[0] - p0[0];
const dy = p1[1] - p0[1];
const segmentLength = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
segments.push({
p0, p1, length: segmentLength, angle
});
}
// Sample points along each segment
for (const segment of segments) {
// Number of points to sample depends on segment length
const numSamplePoints = Math.max(2, Math.ceil(segment.length / 5)); // 1 point every 5 pixels
for (let i = 0; i < numSamplePoints; i++) {
const t = i / (numSamplePoints - 1);
const x = segment.p0[0] + t * (segment.p1[0] - segment.p0[0]);
const y = segment.p0[1] + t * (segment.p1[1] - segment.p0[1]);
points.push([x, y, segment.angle]);
}
}
return points;
}
// Function to get points along the perimeter of a closed shape
function getClosedShapePoints(element) {
let points = [];
if (element.type === "ellipse") {
const centerX = element.x + element.width / 2;
const centerY = element.y + element.height / 2;
const rx = element.width / 2;
const ry = element.height / 2;
// Sample points along the ellipse perimeter
const numPoints = 64;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * 2 * Math.PI;
const x = centerX + rx * Math.cos(angle);
const y = centerY + ry * Math.sin(angle);
// Calculate tangent angle
const tangentAngle = angle + Math.PI / 2; // Tangent is perpendicular to radius
points.push([x, y, tangentAngle]);
}
// Close the loop
points.push(points[0]);
}
else if (element.type === "rectangle" || element.type === "diamond") {
let corners;
if (element.type === "rectangle") {
const x = element.x;
const y = element.y;
const width = element.width;
const height = element.height;
corners = [
[x, y], // top-left
[x, y + height], // bottom-left
[x + width, y + height], // bottom-right
[x + width, y], // top-right
[x, y] // back to start
];
}
else { // Diamond
const x = element.x;
const y = element.y;
const width = element.width;
const height = element.height;
const centerX = x + width / 2;
const centerY = y + height / 2;
corners = [
[centerX, y], // top
[x + width, centerY], // right
[centerX, y + height], // bottom
[x, centerY], // left
[centerX, y] // back to top
];
}
// Sample points along each side of the polygon
for (let i = 0; i < corners.length - 1; i++) {
const [x1, y1] = corners[i];
const [x2, y2] = corners[i + 1];
const dx = x2 - x1;
const dy = y2 - y1;
const sideLength = Math.sqrt(dx*dx + dy*dy);
const angle = Math.atan2(dy, dx);
// Sample points based on side length
const numPoints = Math.max(2, Math.ceil(sideLength / 5)); // 1 point every 5 pixels
for (let j = 0; j < numPoints; j++) {
const t = j / (numPoints - 1);
const x = x1 + t * dx;
const y = y1 + t * dy;
// Fix: Don't add an additional 90 degrees for rectangle and diamond
points.push([x, y, angle]);
}
}
}
return points;
}
// Function to distribute text along any path
function distributeTextAlongPath(text, pathPoints, pathID, objectIDs, charWidths, charHeights, spacing, offset = 0) {
if (pathPoints.length === 0) return;
// Calculate path length
let pathLength = 0;
let pathSegments = [];
let accumulatedLength = 0;
for (let i = 1; i < pathPoints.length; i++) {
const [x1, y1] = [pathPoints[i-1][0], pathPoints[i-1][1]];
const [x2, y2] = [pathPoints[i][0], pathPoints[i][1]];
const segLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
pathSegments.push({
startPoint: pathPoints[i-1],
endPoint: pathPoints[i],
length: segLength,
startDist: accumulatedLength,
endDist: accumulatedLength + segLength
});
accumulatedLength += segLength;
pathLength += segLength;
}
// Total length needed for text with natural spacing
const totalTextLength = charWidths.reduce((sum, width) => sum + width, 0) +
(text.length - 1) * spacing;
// Place characters with natural spacing
// Apply the offset to the starting position
let currentDist = offset;
for (let i = 0; i < text.length; i++) {
const character = text.substring(i, i+1);
const charWidth = charWidths[i];
// Find point on path for this character
let pointInfo = getPointAtDistance(currentDist, pathSegments, pathLength);
let x, y, angle;
if (pointInfo) {
x = pointInfo.x;
y = pointInfo.y;
angle = pointInfo.angle;
} else {
// We're beyond the path, continue in the direction of the last segment
const lastSegment = pathSegments[pathSegments.length - 1];
const lastPoint = lastSegment.endPoint;
const secondLastPoint = lastSegment.startPoint;
angle = Math.atan2(
lastPoint[1] - secondLastPoint[1],
lastPoint[0] - secondLastPoint[0]
);
// Calculate how far past the end of the path
const distanceFromEnd = currentDist - pathLength;
// Position character extending beyond the path
x = lastPoint[0] + Math.cos(angle) * distanceFromEnd;
y = lastPoint[1] + Math.sin(angle) * distanceFromEnd;
}
// Add the character to the drawing
ea.style.angle = angle;
const charPixelWidth = charWidths[i];
const charPixelHeight = charHeights[i];
const charID = ea.addText(x - charPixelWidth/2, y - charPixelHeight/2, character);
ea.addAppendUpdateCustomData(charID, {
text2Path: {pathID, text, pathElID, offset}
});
objectIDs.push(charID);
// Move to next character position with natural spacing
currentDist += charWidth + spacing;
}
transposeElements(new Set(objectIDs));
}
// Helper function to find a point at a specific distance along the path
function getPointAtDistance(distance, segments, totalLength) {
if (distance > totalLength) return null;
// Find the segment where this distance falls
const segment = segments.find(seg =>
distance >= seg.startDist && distance <= seg.endDist
);
if (!segment) return null;
// Calculate position within the segment
const t = (distance - segment.startDist) / segment.length;
const [x1, y1, angle1] = segment.startPoint;
const [x2, y2, angle2] = segment.endPoint;
// Linear interpolation
const x = x1 + t * (x2 - x1);
const y = y1 + t * (y2 - y1);
// Use the segment's angle
const angle = angle1;
return { x, y, angle };
}
async function convertBezierToPoints() {
async function getSVGForPath() {
ea.copyViewElementsToEAforEditing([pathEl]);
const el = ea.getElements()[0];
el.roughness = 0;
const svgDoc = await ea.createSVG();
ea.clear();
return svgDoc;
}
const svgDoc = await getSVGForPath();
// --- Add below: create a line element from the SVG path ---
if (svgDoc) {
// Find the <path> element in the SVG
const pathElSVG = svgDoc.querySelector('path');
if (pathElSVG) {
// Use SVGPathElement's getPointAtLength to sample points along the path
function samplePathPoints(pathElSVG, step = 15) {
const points = [];
const totalLength = pathElSVG.getTotalLength();
for (let len = 0; len <= totalLength; len += step) {
const pt = pathElSVG.getPointAtLength(len);
points.push([pt.x, pt.y]);
}
// Ensure last point is included
const lastPt = pathElSVG.getPointAtLength(totalLength);
if (
points.length === 0 ||
points[points.length - 1][0] !== lastPt.x ||
points[points.length - 1][1] !== lastPt.y
) {
points.push([lastPt.x, lastPt.y]);
}
return points;
}
let points = samplePathPoints(pathElSVG, 15); // 15 px step, adjust for smoothness
// --- Align the new line's first point to the original element's first point ---
// Find the original element's first point (relative to its x/y)
const origFirst = pathEl.type === "rectangle"
? [pathEl.x, pathEl.y]
: (pathEl.type === "diamond"
? [pathEl.x + pathEl.width/2, pathEl.y]
: [pathEl.x + pathEl.points[0][0], pathEl.y + pathEl.points[0][1]]);
// Find the SVG's first point (relative to its g transform)
// Get the <g> transform
const g = pathElSVG.closest('g');
let dx = 0, dy = 0;
if (g) {
const m = g.getAttribute('transform');
// Parse translate(x y)
const match = m && m.match(/translate\(([-\d.]+)[ ,]([-\d.]+)/);
if (match) {
dx = parseFloat(match[1]);
dy = parseFloat(match[2]);
}
}
// SVG points are relative to the group transform
const svgFirst = [points[0][0] + dx, points[0][1] + dy];
// Calculate delta
const deltaX = origFirst[0] - svgFirst[0];
const deltaY = origFirst[1] - svgFirst[1];
// Apply delta to all points
points = points.map(([x, y]) => [x + dx + deltaX, y + dy + deltaY]);
// Trim the very last point
if (points.length > 2) {
points = points.slice(0, -1);
}
if (points.length > 1) {
ea.clear();
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
line.isDeleted = true;
return line;
} else {
new Notice("Could not extract enough points from SVG path.");
}
} else {
new Notice("No path element found in SVG.");
}
}
return pathEl;
}

476
ea-scripts/To Line.md Normal file
View File

@@ -0,0 +1,476 @@
/**
* Converts an ellipse element to a line element
* @param {Object} ellipse - The ellipse element to convert
* @param {number} pointDensity - Optional number of points to generate (defaults to 64)
* @returns {string} The ID of the created line element
```js*/
function ellipseToLine(ellipse, pointDensity = 64) {
if (!ellipse || ellipse.type !== "ellipse") {
throw new Error("Input must be an ellipse element");
}
// Calculate points along the ellipse perimeter
const stepSize = (Math.PI * 2) / pointDensity;
const points = drawEllipse(
ellipse.x,
ellipse.y,
ellipse.width,
ellipse.height,
ellipse.angle,
0,
Math.PI * 2,
stepSize
);
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: ellipse.strokeColor,
strokeWidth: ellipse.strokeWidth,
backgroundColor: ellipse.backgroundColor,
fillStyle: ellipse.fillStyle,
roughness: ellipse.roughness,
strokeSharpness: ellipse.strokeSharpness,
frameId: ellipse.frameId,
groupIds: [...ellipse.groupIds],
opacity: ellipse.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply ellipse styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
// Helper function from the Split Ellipse script
function drawEllipse(x, y, width, height, angle = 0, start = 0, end = Math.PI*2, step = Math.PI/32) {
const ellipse = (t) => {
const spanningVector = rotateVector([width/2*Math.cos(t), height/2*Math.sin(t)], angle);
const baseVector = [x+width/2, y+height/2];
return addVectors([baseVector, spanningVector]);
}
if(end <= start) end = end + Math.PI*2;
let points = [];
const almostEnd = end - step/2;
for (let t = start; t < almostEnd; t = t + step) {
points.push(ellipse(t));
}
points.push(ellipse(end));
return points;
}
function rotateVector(vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function addVectors(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
}
/**
* Converts a rectangle element to a line element
* @param {Object} rectangle - The rectangle element to convert
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
* @returns {string} The ID of the created line element
*/
function rectangleToLine(rectangle, pointDensity = 16) {
if (!rectangle || rectangle.type !== "rectangle") {
throw new Error("Input must be a rectangle element");
}
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: rectangle.strokeColor,
strokeWidth: rectangle.strokeWidth,
backgroundColor: rectangle.backgroundColor,
fillStyle: rectangle.fillStyle,
roughness: rectangle.roughness,
strokeSharpness: rectangle.strokeSharpness,
frameId: rectangle.frameId,
groupIds: [...rectangle.groupIds],
opacity: rectangle.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply rectangle styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Calculate points for the rectangle perimeter
const points = generateRectanglePoints(rectangle, pointDensity);
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
// Helper function to generate rectangle points with optional rounded corners
function generateRectanglePoints(rectangle, pointDensity) {
const { x, y, width, height, angle = 0 } = rectangle;
const centerX = x + width / 2;
const centerY = y + height / 2;
// If no roundness, create a simple rectangle
if (!rectangle.roundness) {
const corners = [
[x, y], // top-left
[x + width, y], // top-right
[x + width, y + height], // bottom-right
[x, y + height], // bottom-left
[x,y] //origo
];
// Apply rotation if needed
if (angle !== 0) {
return corners.map(point => rotatePoint(point, [centerX, centerY], angle));
}
return corners;
}
// Handle rounded corners
const points = [];
// Calculate corner radius using Excalidraw's algorithm
const cornerRadius = getCornerRadius(Math.min(width, height), rectangle);
const clampedRadius = Math.min(cornerRadius, width / 2, height / 2);
// Corner positions
const topLeft = [x + clampedRadius, y + clampedRadius];
const topRight = [x + width - clampedRadius, y + clampedRadius];
const bottomRight = [x + width - clampedRadius, y + height - clampedRadius];
const bottomLeft = [x + clampedRadius, y + height - clampedRadius];
// Add top-left corner arc
points.push(...createArc(
topLeft[0], topLeft[1], clampedRadius, Math.PI, Math.PI * 1.5, pointDensity));
// Add top edge
points.push([x + clampedRadius, y], [x + width - clampedRadius, y]);
// Add top-right corner arc
points.push(...createArc(
topRight[0], topRight[1], clampedRadius, Math.PI * 1.5, Math.PI * 2, pointDensity));
// Add right edge
points.push([x + width, y + clampedRadius], [x + width, y + height - clampedRadius]);
// Add bottom-right corner arc
points.push(...createArc(
bottomRight[0], bottomRight[1], clampedRadius, 0, Math.PI * 0.5, pointDensity));
// Add bottom edge
points.push([x + width - clampedRadius, y + height], [x + clampedRadius, y + height]);
// Add bottom-left corner arc
points.push(...createArc(
bottomLeft[0], bottomLeft[1], clampedRadius, Math.PI * 0.5, Math.PI, pointDensity));
// Add left edge
points.push([x, y + height - clampedRadius], [x, y + clampedRadius]);
// Apply rotation if needed
if (angle !== 0) {
return points.map(point => rotatePoint(point, [centerX, centerY], angle));
}
return points;
}
// Helper function to create an arc of points
function createArc(centerX, centerY, radius, startAngle, endAngle, pointDensity) {
const points = [];
const angleStep = (endAngle - startAngle) / pointDensity;
for (let i = 0; i <= pointDensity; i++) {
const angle = startAngle + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push([x, y]);
}
return points;
}
// Helper function to rotate a point around a center
function rotatePoint(point, center, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Translate point to origin
const x = point[0] - center[0];
const y = point[1] - center[1];
// Rotate point
const xNew = x * cos - y * sin;
const yNew = x * sin + y * cos;
// Translate point back
return [xNew + center[0], yNew + center[1]];
}
}
function getCornerRadius(x, element) {
const fixedRadiusSize = element.roundness?.value ?? 32;
const CUTOFF_SIZE = fixedRadiusSize / 0.25;
if (x <= CUTOFF_SIZE) {
return x * 0.25;
}
return fixedRadiusSize;
}
/**
* Converts a diamond element to a line element
* @param {Object} diamond - The diamond element to convert
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
* @returns {string} The ID of the created line element
*/
function diamondToLine(diamond, pointDensity = 16) {
if (!diamond || diamond.type !== "diamond") {
throw new Error("Input must be a diamond element");
}
// Save original styling to apply to the new line
const originalStyling = {
strokeColor: diamond.strokeColor,
strokeWidth: diamond.strokeWidth,
backgroundColor: diamond.backgroundColor,
fillStyle: diamond.fillStyle,
roughness: diamond.roughness,
strokeSharpness: diamond.strokeSharpness,
frameId: diamond.frameId,
groupIds: [...diamond.groupIds],
opacity: diamond.opacity
};
// Use current style
const prevStyle = {...ea.style};
// Apply diamond styling to the line
ea.style.strokeColor = originalStyling.strokeColor;
ea.style.strokeWidth = originalStyling.strokeWidth;
ea.style.backgroundColor = originalStyling.backgroundColor;
ea.style.fillStyle = originalStyling.fillStyle;
ea.style.roughness = originalStyling.roughness;
ea.style.strokeSharpness = originalStyling.strokeSharpness;
ea.style.opacity = originalStyling.opacity;
// Calculate points for the diamond perimeter
const points = generateDiamondPoints(diamond, pointDensity);
// Create the line and close it
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
// Make it a polygon to close the path
line.polygon = true;
// Transfer grouping and frame information
line.frameId = originalStyling.frameId;
line.groupIds = originalStyling.groupIds;
// Restore previous style
ea.style = prevStyle;
return lineId;
function generateDiamondPoints(diamond, pointDensity) {
const { x, y, width, height, angle = 0 } = diamond;
const cx = x + width / 2;
const cy = y + height / 2;
// Diamond corners
const top = [cx, y];
const right = [x + width, cy];
const bottom = [cx, y + height];
const left = [x, cy];
if (!diamond.roundness) {
const corners = [top, right, bottom, left, top];
if (angle !== 0) {
return corners.map(pt => rotatePoint(pt, [cx, cy], angle));
}
return corners;
}
// Clamp radius
const r = Math.min(
getCornerRadius(Math.min(width, height) / 2, diamond),
width / 2,
height / 2
);
// For a diamond, the rounded corner is a *bezier* between the two adjacent edge points, not a circular arc.
// Excalidraw uses a quadratic bezier for each corner, with the control point at the corner itself.
// Calculate edge directions
function sub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function add(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function norm([x, y]) {
const len = Math.hypot(x, y);
return [x / len, y / len];
}
function scale([x, y], s) { return [x * s, y * s]; }
// For each corner, move along both adjacent edges by r to get arc endpoints
// Order: top, right, bottom, left
const corners = [top, right, bottom, left];
const next = [right, bottom, left, top];
const prev = [left, top, right, bottom];
// For each corner, calculate the two points where the straight segments meet the arc
const arcPoints = [];
for (let i = 0; i < 4; ++i) {
const c = corners[i];
const n = next[i];
const p = prev[i];
const toNext = norm(sub(n, c));
const toPrev = norm(sub(p, c));
arcPoints.push([
add(c, scale(toPrev, r)), // start of arc (from previous edge)
add(c, scale(toNext, r)), // end of arc (to next edge)
c // control point for bezier
]);
}
// Helper: quadratic bezier between p0 and p2 with control p1
function bezier(p0, p1, p2, density) {
const pts = [];
for (let i = 0; i <= density; ++i) {
const t = i / density;
const mt = 1 - t;
pts.push([
mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0],
mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1]
]);
}
return pts;
}
// Build path: for each corner, straight line to arc start, then bezier to arc end using corner as control
let pts = [];
for (let i = 0; i < 4; ++i) {
const prevArc = arcPoints[(i + 3) % 4];
const arc = arcPoints[i];
if (i === 0) {
pts.push(arc[0]);
} else {
pts.push(arc[0]);
}
// Quadratic bezier from arc[0] to arc[1] with control at arc[2] (the corner)
pts.push(...bezier(arc[0], arc[2], arc[1], pointDensity));
}
pts.push(arcPoints[0][0]); // close
if (angle !== 0) {
return pts.map(pt => rotatePoint(pt, [cx, cy], angle));
}
return pts;
}
// Helper function to create an arc between two points
function createArcBetweenPoints(startPoint, endPoint, centerX, centerY, pointDensity) {
const startAngle = Math.atan2(startPoint[1] - centerY, startPoint[0] - centerX);
const endAngle = Math.atan2(endPoint[1] - centerY, endPoint[0] - centerX);
// Ensure angles are in correct order for arc drawing
let adjustedEndAngle = endAngle;
if (endAngle < startAngle) {
adjustedEndAngle += 2 * Math.PI;
}
const points = [];
const angleStep = (adjustedEndAngle - startAngle) / pointDensity;
// Start with the straight line to arc start
points.push(startPoint);
// Create arc points
for (let i = 1; i < pointDensity; i++) {
const angle = startAngle + i * angleStep;
const distance = Math.hypot(startPoint[0] - centerX, startPoint[1] - centerY);
const x = centerX + distance * Math.cos(angle);
const y = centerY + distance * Math.sin(angle);
points.push([x, y]);
}
// Add the end point of the arc
points.push(endPoint);
return points;
}
// Helper function to rotate a point around a center
function rotatePoint(point, center, angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Translate point to origin
const x = point[0] - center[0];
const y = point[1] - center[1];
// Rotate point
const xNew = x * cos - y * sin;
const yNew = x * sin + y * cos;
// Translate point back
return [xNew + center[0], yNew + center[1]];
}
}
const el = ea.getViewSelectedElement();
switch (el.type) {
case "rectangle":
rectangleToLine(el);
break;
case "ellipse":
ellipseToLine(el);
break;
case "diamond":
diamondToLine(el);
break;
}
ea.addElementsToView();

File diff suppressed because one or more lines are too long

View File

@@ -73,8 +73,8 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.svg"/></div>|[[#Text to Path]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
## Styling and Appearance
@@ -596,24 +596,24 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Split%20text%20by%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Split lines of text into separate text elements for easier reorganization<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg'></td></tr></table>
## Text Arch
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to fit a text element along a selected path (line, arrow, freedraw, ellipse, rectangle, or diamond) in Excalidraw. You can select either a path or a text element, or both:<br><br>
- If only a path is selected, you will be prompted to provide the text.<br>
- If only a text element is selected and it was previously fitted to a path, the script will use the original path if it is still present in the scene.<br>
- If both a text and a path are selected, the script will fit the text to the selected path.<br><br>
If the path is a perfect circle, you will be prompted to choose whether to fit the text above or below the circle.<br><br>
After fitting, the text will no longer be editable as a standard text element or function as a markdown link. Emojis are not supported.<br>
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
## Text Aura
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
## Text to Path
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20to%20Path.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to fit a text element along a selected path: line, arrow, freedraw, ellipse, rectangle, or diamond. You can select either a path or a text element, or both:<br><br>
- If only a path is selected, you will be prompted to provide the text.<br>
- If only a text element is selected and it was previously fitted to a path, the script will use the original path if it is still present in the scene.<br>
- If both a text and a path are selected, the script will fit the text to the selected path.<br><br>
If the path is a perfect circle, you will be prompted to choose whether to fit the text above or below the circle.<br><br>
After fitting, the text will no longer be editable as a standard text element or function as a markdown link. Emojis are not supported.<br>
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-to-path.jpg'></td></tr></table>
## Toggle Grid
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -23,7 +23,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.18.0-16",
"@zsviczian/excalidraw": "0.18.0-17",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -566,7 +566,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
this.packageManager.getPackageMap().forEach(({excalidrawLib}) => {
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL);
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any}, fourthFontDataURL);
});
// Add fonts to open Obsidian documents
for(const ownerDocument of this.getOpenObsidianDocuments()) {

View File

@@ -749,7 +749,7 @@ FILENAME_HEAD: "Filename",
"ExcalidrawAutomate is a scripting and automation API for Excalidraw. Unfortunately, the documentation of the API is sparse. " +
"I recommend reading the <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> file, " +
"visiting the <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> page - though the information " +
"here has not been updated for a long while -, and finally to enable the field suggester below. The field suggester will show you the available " +
"here has not been updated for a long while -, and finally to enable the field suggester below. The field suggester will show you the available " +
"functions, their parameters and short description as you type. The field suggester is the most up-to-date documentation of the API.",
FIELD_SUGGESTER_NAME: "Enable Field Suggester",
FIELD_SUGGESTER_DESC:
@@ -991,6 +991,7 @@ FILENAME_HEAD: "Filename",
//Utils.ts
UPDATE_AVAILABLE: `A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is`,
SCRIPT_UPDATES_AVAILABLE: `Script updates available - check the script store.\n\n${DEVICE.isDesktop ? `This message is available in console.log (${DEVICE.isMacOS ? "CMD+OPT+i" : "CTRL+SHIFT+i"})\n\n` : ""}If you have organized scripts into subfolders under the script store folder and have multiple copies of the same script, you may need to clean up unused versions to clear this alert. For private copies of scripts that should not be updated, store them outside the script store folder.`,
ERROR_PNG_TOO_LARGE: "Error exporting PNG - PNG too large, try a smaller resolution",
//modifierkeyHelper.ts

View File

@@ -29,6 +29,8 @@ I build this plugin in my free time, as a labor of love. Curious about the philo
- If you enter line editor mode by CTRL/CMD + clicking on the line, the lock-point will be marked for easier editing. Look for the polygon action on the elements panel to break the polygon if required.
- Override "Focus on Existing Tab" setting for the "Open the back-of-the-note for the selected image in a popout window" action. "Open the back-of-the-note" will open a new popout every time.
- I significantly extended the features of the Text Arch script. It can now fit text to a wide range of paths and shapes, and allows for the text to be edited and refitted to other paths.
- Better element unlock. Single left click on a locked element will display an unlock button. [#9546](https://github.com/excalidraw/excalidraw/pull/9546)
- On startup Excalidraw will alert you if there are installed scripts that have available updates.
`,
"2.11.1": `
## Fixed:

View File

@@ -1,8 +1,8 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/src";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/src/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/FontMetadata";
import { FontMetadata } from "@zsviczian/excalidraw/types/common/src";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/common/src/utility-types";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/src/types";
@@ -146,6 +146,15 @@ declare namespace ExcalidrawLib {
elementsMap: ElementsMap,
): ExcalidrawElement[][];
function getFontMetrics(fontFamily: ExcalidrawTextElement["fontFamily"], fontSize?:number): {
unitsPerEm: number,
ascender: number,
descender: number,
lineHeight: number,
baseline: number,
fontString: string
}
function measureText(
text: string,
font: FontString,

View File

@@ -27,6 +27,7 @@ export interface ViewSemaphores {
//flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing
embeddableIsEditingSelf: boolean;
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
viewloaded: boolean; //onLayoutReady in view.onload has completed.
viewunload: boolean;
//first time initialization of the view
scriptsReady: boolean;

View File

@@ -16,6 +16,7 @@ import {
getCommonBoundingBox,
DEVICE,
getContainerElement,
SCRIPT_INSTALL_FOLDER,
} from "../constants/constants";
import ExcalidrawPlugin from "../core/main";
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/element/src/types";
@@ -33,6 +34,7 @@ import Pool from "es6-promise-pool";
import { FileData } from "../shared/EmbeddedFileLoader";
import { t } from "src/lang/helpers";
import ExcalidrawScene from "src/shared/svgToExcalidraw/elements/ExcalidrawScene";
import { log } from "./debugHelper";
declare const PLUGIN_VERSION:string;
declare var LZString: any;
@@ -82,6 +84,9 @@ export async function checkExcalidrawVersion() {
t("UPDATE_AVAILABLE") + ` ${latestVersion}`,
);
}
// Check for script updates
await checkScriptUpdates();
} catch (e) {
console.log({ where: "Utils/checkExcalidrawVersion", error: e });
}
@@ -91,6 +96,56 @@ export async function checkExcalidrawVersion() {
}, 28800000); //reset after 8 hours
};
async function checkScriptUpdates() {
try {
if (!EXCALIDRAW_PLUGIN?.settings?.scriptFolderPath) {
return;
}
const folder = `${EXCALIDRAW_PLUGIN.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
const installedScripts = EXCALIDRAW_PLUGIN.app.vault.getFiles()
.filter(f => f.path.startsWith(folder) && f.extension === "md");
if (installedScripts.length === 0) {
return;
}
// Get directory info from GitHub
const files = new Map<string, number>();
const directoryInfo = JSON.parse(
await request({
url: "https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/directory-info.json",
}),
);
directoryInfo.forEach((f: any) => files.set(f.fname, f.mtime));
if (files.size === 0) {
return;
}
// Check if any installed scripts have updates
const updates:string[] = [];
let hasUpdates = false;
for (const scriptFile of installedScripts) {
const filename = scriptFile.name;
if (files.has(filename)) {
const mtime = files.get(filename);
if (mtime > scriptFile.stat.mtime) {
updates.push(scriptFile.path.split(folder)?.[1]?.split(".md")[0]);
hasUpdates = true;
}
}
}
if (hasUpdates) {
const message = `${t("SCRIPT_UPDATES_AVAILABLE")}\n\n${updates.sort().join("\n")}`;
new Notice(message,8000+updates.length*1000);
log(message);
}
} catch (e) {
console.log({ where: "Utils/checkScriptUpdates", error: e });
}
}
const random = new Random(Date.now());
export function randomInteger () {

View File

@@ -315,6 +315,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
warnAboutLinearElementLinkClick: true,
embeddableIsEditingSelf: false,
popoutUnload: false,
viewloaded: false,
viewunload: false,
scriptsReady: false,
justLoaded: false,
@@ -1656,8 +1657,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(!Boolean(this?.plugin?.activeLeafChangeEventHandler)) return;
if (Boolean(this.plugin.activeLeafChangeEventHandler) && (this?.app?.workspace?.activeLeaf === this.leaf)) {
this.plugin.activeLeafChangeEventHandler(this.leaf);
}
this.canvasNodeFactory.initialize();
}
await this.canvasNodeFactory.initialize();
this.contentEl.addClass("excalidraw-view");
//https://github.com/zsviczian/excalibrain/issues/28
await this.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
@@ -1694,6 +1695,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false);
//this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2004
this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false);
this.semaphores.viewloaded = true;
});
this.setupAutosaveTimer();
@@ -2359,7 +2361,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
await this.plugin.awaitInit();
let counter = 0;
while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
while ((!this.semaphores.viewloaded || !this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
if(!this.file) return;
this.compatibilityMode = this.file.extension === "excalidraw";
await this.plugin.loadSettings();

View File

@@ -50,17 +50,17 @@ export class CanvasNodeFactory {
}
public async initialize() {
//@ts-ignore
const app = this.view.app;
const canvasPlugin = app.internalPlugins.plugins["canvas"];
if(!canvasPlugin._loaded) {
await canvasPlugin.load();
}
const doc = this.view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(this.view.app.workspace, "vertical");
rootSplit.getRoot = () => this.view.app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
rootSplit.getContainer = () => getContainerForDocument(doc);
this.leaf = this.view.app.workspace.createLeafInParent(rootSplit, 0);
this.leaf = app.workspace.createLeafInParent(rootSplit, 0);
this.canvas = canvasPlugin.views.canvas(this.leaf).canvas;
this.initialized = true;
}