mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
7 Commits
2.11.0
...
2.12.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6824a1aa68 | ||
|
|
00de9d639b | ||
|
|
5a66c78428 | ||
|
|
d1be193125 | ||
|
|
f4c8d21a33 | ||
|
|
d588f749d2 | ||
|
|
c1f909427b |
@@ -69,10 +69,15 @@ const result = polyboolAction({
|
||||
const polygonHierachy = subordinateInnerPolygons(result.regions);
|
||||
drawPolygonHierachy(polygonHierachy);
|
||||
ea.deleteViewElements(elements);
|
||||
setPolygonTrue();
|
||||
ea.addElementsToView(false,false,true);
|
||||
return;
|
||||
|
||||
|
||||
function setPolygonTrue() {
|
||||
ea.getElements().filter(el=>el.type==="line").forEach(el => {
|
||||
el.polygon = true;
|
||||
});
|
||||
}
|
||||
|
||||
function traceElement(element) {
|
||||
const diamondPath = (diamond) => [
|
||||
|
||||
@@ -109,9 +109,18 @@ async function editExistingTextElement(elements) {
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt(
|
||||
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true,true
|
||||
);
|
||||
const text = await utils.inputPrompt({
|
||||
header: "Edit text",
|
||||
placeholder: "",
|
||||
value: elements[0].rawText,
|
||||
//buttons: undefined,
|
||||
lines: 5,
|
||||
displayEditorButtons: true,
|
||||
customComponents: customControls,
|
||||
blockPointerInputOutsideModal: true,
|
||||
controlsOnTop: true
|
||||
});
|
||||
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.
|
||||
There is also the option to close the object along the cut, which will close the cut in the shape of the line.
|
||||

|
||||

|
||||

|
||||

|
||||
Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ angles.forEach((angle, key) => {
|
||||
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
if (closeObject && cuttingLine) line.polygon = true;
|
||||
line.frameId = ellipse.frameId;
|
||||
line.groupIds = ellipse.groupIds;
|
||||
});
|
||||
|
||||
@@ -1,49 +1,613 @@
|
||||
/*
|
||||

|
||||
|
||||
Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.
|
||||
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
|
||||
*/
|
||||
el = ea.getViewSelectedElement();
|
||||
if(!el || el.type!=="text") {
|
||||
new Notice("Please select a text element");
|
||||
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;
|
||||
}
|
||||
|
||||
ea.style.fontSize = el.fontSize;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.opacity = el.opacity;
|
||||
// -------------------------------------
|
||||
// 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;
|
||||
|
||||
const r = parseInt (await utils.inputPrompt("The radius of the arch you'd like to fit the text to","number","150"));
|
||||
const archAbove = await utils.suggester(["Arch above","Arch below"],[true,false]);
|
||||
// -----------------------------------
|
||||
// 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;});
|
||||
}
|
||||
}
|
||||
|
||||
if(isNaN(r)) {
|
||||
new Notice("The radius is not a number");
|
||||
// --------------------------------------------------------
|
||||
// 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;
|
||||
}
|
||||
|
||||
circlePoint = (angle) => archAbove
|
||||
? [
|
||||
r * Math.sin(angle),
|
||||
-r * Math.cos(angle)
|
||||
]
|
||||
: [
|
||||
-r * Math.sin(angle),
|
||||
r * Math.cos(angle)
|
||||
];
|
||||
// ------------------------------------------------------------
|
||||
// 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);
|
||||
|
||||
let rot = (archAbove ? -0.5 : 0.5) * ea.measureText(el.text).width/r;
|
||||
|
||||
// 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 = [];
|
||||
for(i=0;i<el.text.length;i++) {
|
||||
const character = el.text.substring(i,i+1);
|
||||
const width = ea.measureText(character).width;
|
||||
ea.style.angle = rot;
|
||||
const [x,y] = circlePoint(rot);
|
||||
rot += (archAbove ? 1 : -1) *width / r;
|
||||
objectIDs.push(ea.addText(x,y,character));
|
||||
|
||||
// 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;
|
||||
})
|
||||
}
|
||||
ea.addToGroup(objectIDs);
|
||||
ea.addElementsToView(true, false, true);
|
||||
|
||||
// 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;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -600,7 +600,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```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">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
|
||||
<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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0-beta-2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.11.0",
|
||||
"version": "2.11.1",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
"@zsviczian/excalidraw": "0.18.0-12",
|
||||
"@zsviczian/excalidraw": "0.18.0-14",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"es6-promise-pool": "2.5.0",
|
||||
@@ -3198,9 +3198,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zsviczian/excalidraw": {
|
||||
"version": "0.18.0-12",
|
||||
"resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.18.0-12.tgz",
|
||||
"integrity": "sha512-/jAR1aGtnkYO4fT4wBUhItrWAnu1otF4UksOxO96r2za51kB5vc9z5lPdVDGctuchaPU4MuyeC8cVk3K2iNKzw==",
|
||||
"version": "0.18.0-14",
|
||||
"resolved": "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.18.0-14.tgz",
|
||||
"integrity": "sha512-LXmlImnsYxXzObnO0YxPNl4+TeLyM5BhIwUUGxdL/GY+Ru4tTzbwGf7l5GaVsWejrCYvVWM6j/wDwJuL6V1s9Q==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.18.0-13",
|
||||
"@zsviczian/excalidraw": "0.18.0-16",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
|
||||
@@ -1078,6 +1078,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.eventManager.onActiveLeafChangeHandler(leaf);
|
||||
}
|
||||
|
||||
public setDebounceActiveLeafChangeHandler() {
|
||||
this.eventManager.setDebounceActiveLeafChangeHandler();
|
||||
}
|
||||
|
||||
public registerHotkeyOverrides() {
|
||||
//this is repeated here because the same function is called when settings is closed after hotkeys have changed
|
||||
if (this.popScope) {
|
||||
|
||||
@@ -1038,8 +1038,13 @@ export class CommandManager {
|
||||
x:Math.max(0,centerX - width/2 + view.ownerWindow.screenX),
|
||||
y:Math.max(0,centerY - height/2 + view.ownerWindow.screenY),
|
||||
}
|
||||
|
||||
const focusOnFileTab = this.settings.focusOnFileTab;
|
||||
//override focusOnFileTab for popout windows
|
||||
if(DEVICE.isDesktop) {
|
||||
this.settings.focusOnFileTab = false;
|
||||
}
|
||||
this.plugin.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true, undefined, false, {x,y,width,height});
|
||||
this.settings.focusOnFileTab = focusOnFileTab;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export class EventManager {
|
||||
private removeEventLisnters:(()=>void)[] = []; //only used if I register an event directly, not via Obsidian's registerEvent
|
||||
private previouslyActiveLeaf: WorkspaceLeaf;
|
||||
private splitViewLeafSwitchTimestamp: number = 0;
|
||||
private debunceActiveLeafChangeHandlerTimer: number|null = null;
|
||||
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
@@ -103,6 +104,15 @@ export class EventManager {
|
||||
this.plugin.registerEvent(this.plugin.app.workspace.on("editor-menu", this.onEditorMenuHandler.bind(this)));
|
||||
}
|
||||
|
||||
public setDebounceActiveLeafChangeHandler() {
|
||||
if(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
window.clearTimeout(this.debunceActiveLeafChangeHandlerTimer);
|
||||
}
|
||||
this.debunceActiveLeafChangeHandlerTimer = window.setTimeout(() => {
|
||||
this.debunceActiveLeafChangeHandlerTimer = null;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private onLayoutChangeHandler() {
|
||||
getExcalidrawViews(this.app).forEach(excalidrawView=>excalidrawView.refresh());
|
||||
}
|
||||
@@ -164,6 +174,10 @@ export class EventManager {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onActiveLeafChangeHandler,`onActiveLeafChangeEventHandler`, leaf);
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
||||
|
||||
if(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
//In Obsidian 1.8.x the active excalidraw leaf is obscured by an empty leaf without a parent
|
||||
//This hack resolves it
|
||||
if(this.app.workspace.activeLeaf === leaf && isUnwantedLeaf(leaf)) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
|
||||
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
|
||||
TOGGLE_SPLASHSCREEN: "Show splash screen in new drawings",
|
||||
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
|
||||
FLIP_IMAGE: "Open the back-of-the-note for the selected image in a popout window",
|
||||
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
|
||||
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
|
||||
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",
|
||||
@@ -463,7 +463,7 @@ FILENAME_HEAD: "Filename",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "Focus on Existing Tab",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "When opening a link, Excalidraw will focus on the existing tab if the file is already open. " +
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open.",
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open except for the 'Open the back-of-the-note of the selected excalidraw image' command palette action.",
|
||||
SECOND_ORDER_LINKS_NAME: "Show second-order links",
|
||||
SECOND_ORDER_LINKS_DESC: "Show links when clicking on a link in Excalidraw. Second-order link are backlinks pointing to the link being clicked. " +
|
||||
"When using image icons to connect similar notes, second order links allow you to get to related notes in one click instead of two. " +
|
||||
|
||||
@@ -961,6 +961,7 @@ FILENAME_HEAD: "文件名",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "插入空格",
|
||||
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
|
||||
PROMPT_BUTTON_UPPERCASE: "大写",
|
||||
PROMPT_BUTTON_SPECIAL_CHARS : "特殊字符" ,
|
||||
PROMPT_SELECT_TEMPLATE: "选择一个模板",
|
||||
|
||||
//ModifierKeySettings
|
||||
|
||||
@@ -88,9 +88,13 @@ export class ExportDialog extends Modal {
|
||||
this.selectedOnlySetting = null;
|
||||
this.containerEl.remove();
|
||||
}
|
||||
|
||||
get isSelectedOnly(): boolean {
|
||||
return this.hasSelectedElements && this.exportSelectedOnly;
|
||||
}
|
||||
|
||||
updateBoundingBox() {
|
||||
if(this.hasSelectedElements && this.exportSelectedOnly) {
|
||||
if(this.isSelectedOnly) {
|
||||
this.boundingBox = this.ea.getBoundingBox(this.view.getViewSelectedElements());
|
||||
} else {
|
||||
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
|
||||
@@ -368,12 +372,12 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPNG.onclick = () => {
|
||||
if(isScreenshot) {
|
||||
//allow dialot to close before taking screenshot
|
||||
//allow dialog to close before taking screenshot
|
||||
setTimeout(async () => {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -381,7 +385,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.exportPNG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportPNG(this.embedScene, this.isSelectedOnly);
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -398,7 +402,7 @@ export class ExportDialog extends Modal {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -406,7 +410,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
|
||||
this.view.savePNG(this.view.getScene(this.isSelectedOnly));
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -417,12 +421,12 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPNGClipboard.onclick = async () => {
|
||||
if(isScreenshot) {
|
||||
//allow dialot to close before taking screenshot
|
||||
//allow dialog to close before taking screenshot
|
||||
setTimeout(async () => {
|
||||
const png = await captureScreenshot(this.view, {
|
||||
zoom: this.scale,
|
||||
margin: this.padding,
|
||||
selectedOnly: this.exportSelectedOnly,
|
||||
selectedOnly: this.isSelectedOnly,
|
||||
theme: this.theme
|
||||
});
|
||||
if(png) {
|
||||
@@ -430,7 +434,7 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.isSelectedOnly);
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
@@ -452,7 +456,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVG.onclick = () => {
|
||||
this.view.exportSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.view.exportSVG(this.embedScene, this.isSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
@@ -462,7 +466,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVGVault.onclick = () => {
|
||||
this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
|
||||
this.view.saveSVG(this.view.getScene(this.isSelectedOnly));
|
||||
this.close();
|
||||
};
|
||||
|
||||
@@ -471,7 +475,7 @@ export class ExportDialog extends Modal {
|
||||
cls: "excalidraw-export-button"
|
||||
});
|
||||
bSVGClipboard.onclick = async () => {
|
||||
const svg = await this.view.getSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
const svg = await this.view.getSVG(this.embedScene, this.isSelectedOnly);
|
||||
exportSVGToClipboard(svg);
|
||||
this.close();
|
||||
};
|
||||
@@ -504,7 +508,7 @@ export class ExportDialog extends Modal {
|
||||
});
|
||||
bPDFExport.onclick = () => {
|
||||
this.view.exportPDF(
|
||||
this.hasSelectedElements && this.exportSelectedOnly,
|
||||
this.isSelectedOnly,
|
||||
this.pageSize,
|
||||
this.pageOrientation
|
||||
);
|
||||
|
||||
@@ -16,6 +16,28 @@ export const RELEASE_NOTES: { [k: string]: string } = {
|
||||
I build this plugin in my free time, as a labor of love. Curious about the philosophy behind it? Check out [📕 Sketch Your Mind](https://sketch-your-mind.com). If you find it valuable, say THANK YOU or…
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
|
||||
`,
|
||||
"2.12.0": `
|
||||
## Fixed
|
||||
- Dynamic styling was not working when there were frames in the scene.
|
||||
- Minor fix for the screenshot feature. This fix also solves the long-standing issue of the window control buttons (close, minimize, maximize) being visible in full-screen mode.
|
||||
- When ALT/OPT + dragging an Embeddable object, the object dragging was not working properly at times, resulting in an empty embeddable object (instead of dragging the element).
|
||||
|
||||
## New
|
||||
- Line polygons. Draw a closed line shape, and it will snap to a polygon. [#9477](https://github.com/excalidraw/excalidraw/pull/9477)
|
||||
- I updated the Split Ellipse and Boolean Operations Scripts to support this new feature.
|
||||
- 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.
|
||||
`,
|
||||
"2.11.1": `
|
||||
## Fixed:
|
||||
- The new "Screenshot" option in the Export Image dialog was not working properly. [#2339](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2339)
|
||||
|
||||
## New from Excalidraw.com
|
||||
- Quarter snap points for diamonds [#9387](https://github.com/excalidraw/excalidraw/pull/9387)
|
||||
- Precise highlights for bindings [#9472](https://github.com/excalidraw/excalidraw/pull/9472)
|
||||
|
||||
`,
|
||||
"2.11.0": `
|
||||
## New
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DynamicStyle } from "src/types/types";
|
||||
import { cloneElement } from "./excalidrawAutomateUtils";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/element/src/types";
|
||||
import { addAppendUpdateCustomData } from "./utils";
|
||||
import { mutateElement } from "src/constants/constants";
|
||||
import { CaptureUpdateAction } from "src/constants/constants";
|
||||
|
||||
export const setDynamicStyle = (
|
||||
@@ -176,7 +175,7 @@ export const setDynamicStyle = (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
mutateElement(e,{customData: f.customData});
|
||||
(view.excalidrawAPI as ExcalidrawImperativeAPI).mutateElement(e,{customData: f.customData});
|
||||
});
|
||||
|
||||
view.updateScene({
|
||||
|
||||
@@ -698,14 +698,14 @@ export const getTextElementsMatchingQuery = (
|
||||
el.type === "text" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.rawText.toLowerCase().split("\n")[0].trim();
|
||||
const text = el.customData?.text2Path?.text ?? el.rawText.toLowerCase().split("\n")[0].trim();
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
|
||||
const text = el.customData?.text2Path?.text ?? el.rawText.toLowerCase().replaceAll("\n", " ").trim();
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { request } from "http";
|
||||
import { Notice } from "obsidian";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { getEA } from "src/core";
|
||||
@@ -20,12 +19,18 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
return null;
|
||||
}
|
||||
|
||||
(view.excalidrawAPI as ExcalidrawImperativeAPI).setForceRenderAllEmbeddables(true);
|
||||
|
||||
const wasFullscreen = view.isFullscreen();
|
||||
if (!wasFullscreen) {
|
||||
view.gotoFullscreen();
|
||||
}
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
api.setForceRenderAllEmbeddables(true);
|
||||
options.selectedOnly = options.selectedOnly && (view.getViewSelectedElements().length > 0);
|
||||
const remote = window.require("electron").remote;
|
||||
const scene = view.getScene();
|
||||
const viewSelectedElements = view.getViewSelectedElements();
|
||||
const selectedIDs = new Set(options.selectedOnly ? viewSelectedElements.map(el => el.id) : []);
|
||||
const elementsToInclude = options.selectedOnly
|
||||
? view.getViewSelectedElements()
|
||||
: view.getViewElements();
|
||||
const includedElementIDs = new Set(elementsToInclude.map(el => el.id));
|
||||
const savedOpacity: { id: string; opacity: number }[] = [];
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
|
||||
@@ -39,7 +44,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
if (options.selectedOnly) {
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id)));
|
||||
ea.copyViewElementsToEAforEditing(view.getViewElements().filter(el=>!includedElementIDs.has(el.id)));
|
||||
ea.getElements().forEach(el => {
|
||||
savedOpacity.push({
|
||||
id: el.id,
|
||||
@@ -52,7 +57,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
}
|
||||
}
|
||||
|
||||
let boundingBox = ea.getBoundingBox(options.selectedOnly ? viewSelectedElements : scene.elements);
|
||||
let boundingBox = ea.getBoundingBox(elementsToInclude);
|
||||
boundingBox = {
|
||||
topX: Math.ceil(boundingBox.topX),
|
||||
topY: Math.ceil(boundingBox.topY),
|
||||
@@ -61,8 +66,8 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
}
|
||||
|
||||
const margin = options.margin;
|
||||
const availableWidth = Math.floor(view.excalidrawAPI.getAppState().width);
|
||||
const availableHeight = Math.floor(view.excalidrawAPI.getAppState().height);
|
||||
const availableWidth = Math.floor(api.getAppState().width);
|
||||
const availableHeight = Math.floor(api.getAppState().height);
|
||||
|
||||
// Apply zoom to the total dimensions
|
||||
const totalWidth = Math.ceil(boundingBox.width * options.zoom + margin * 2);
|
||||
@@ -89,7 +94,7 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
viewModeEnabled,
|
||||
linkOpacity,
|
||||
theme,
|
||||
} = view.excalidrawAPI.getAppState();
|
||||
} = api.getAppState();
|
||||
return {
|
||||
scrollX,
|
||||
scrollY,
|
||||
@@ -123,10 +128,11 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
|
||||
// Hide UI elements (must be after changing to view mode)
|
||||
const container = view.excalidrawWrapperRef.current;
|
||||
const layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
const layerUIWrapperOriginalDisplay = layerUIWrapper.style.display;
|
||||
layerUIWrapper.style.display = "none";
|
||||
|
||||
let layerUIWrapperOriginalDisplay = "block";
|
||||
let appBottonBarOriginalDisplay = "block";
|
||||
let layerUIWrapper: HTMLElement | null = null;
|
||||
let appBottomBar: HTMLElement | null = null;
|
||||
|
||||
const originalStyle = {
|
||||
width: container.style.width,
|
||||
height: container.style.height,
|
||||
@@ -153,10 +159,12 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(50); // wait for frame to render
|
||||
|
||||
await sleep(200); // wait for frame to render
|
||||
|
||||
// Prepare to collect tile images as data URLs
|
||||
const rect = container.getBoundingClientRect();
|
||||
const { left,top } = container.getBoundingClientRect();
|
||||
//const { offsetLeft, offsetTop } = api.getAppState();
|
||||
|
||||
const tiles = [];
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
@@ -176,16 +184,30 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
height: tileHeight
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await sleep(250);
|
||||
|
||||
//set tileWidth/tileHeight will reset the button bar
|
||||
layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
appBottomBar = container.querySelector(".App-bottom-bar");
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapperOriginalDisplay = layerUIWrapper.style.display;
|
||||
layerUIWrapper.style.display = "none";
|
||||
}
|
||||
if (appBottomBar) {
|
||||
appBottonBarOriginalDisplay = appBottomBar.style.display;
|
||||
appBottomBar.style.display = "none";
|
||||
}
|
||||
|
||||
await sleep(50);
|
||||
|
||||
// Calculate the exact width/height for this tile
|
||||
const captureWidth = col === cols - 1 ? adjustedTotalWidth - tileWidth * (cols - 1) : tileWidth;
|
||||
const captureHeight = row === rows - 1 ? adjustedTotalHeight - tileHeight * (rows - 1) : tileHeight;
|
||||
|
||||
const image = await remote.getCurrentWebContents().capturePage({
|
||||
x: rect.left * devicePixelRatio,
|
||||
y: rect.top * devicePixelRatio,
|
||||
x: left, //offsetLeft,
|
||||
y: top, //offsetTop,
|
||||
width: captureWidth * devicePixelRatio,
|
||||
height: captureHeight * devicePixelRatio,
|
||||
});
|
||||
@@ -243,31 +265,34 @@ export async function captureScreenshot(view: ExcalidrawView, options: Screensho
|
||||
new Notice(t("SCREENSHOT_ERROR"));
|
||||
return null;
|
||||
} finally {
|
||||
// Restore opacity for selected elements if necessary
|
||||
if (options.selectedOnly && savedOpacity.length > 0) {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(view.getViewElements().filter(el => !includedElementIDs.has(el.id)));
|
||||
savedOpacity.forEach(x => {
|
||||
ea.getElement(x.id).opacity = x.opacity;
|
||||
});
|
||||
await ea.addElementsToView(false, false, false, false);
|
||||
}
|
||||
|
||||
// Restore browser zoom to its original value
|
||||
webContents.setZoomFactor(originalZoomFactor);
|
||||
|
||||
// Restore UI elements
|
||||
try {
|
||||
const container = view.excalidrawWrapperRef.current;
|
||||
const layerUIWrapper = container.querySelector(".layer-ui__wrapper");
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapper.style.display = layerUIWrapperOriginalDisplay;
|
||||
}
|
||||
|
||||
// Restore original state
|
||||
restoreState(savedState);
|
||||
|
||||
// Restore opacity for selected elements if necessary
|
||||
if (options.selectedOnly && savedOpacity.length > 0) {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id)));
|
||||
savedOpacity.forEach(x => {
|
||||
ea.getElement(x.id).opacity = x.opacity;
|
||||
});
|
||||
await ea.addElementsToView(false, false, false, false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in cleanup:", e);
|
||||
if (layerUIWrapper) {
|
||||
layerUIWrapper.style.display = layerUIWrapperOriginalDisplay;
|
||||
}
|
||||
|
||||
if (appBottomBar) {
|
||||
appBottomBar.style.display = appBottonBarOriginalDisplay;
|
||||
}
|
||||
|
||||
// Restore original state
|
||||
restoreState(savedState);
|
||||
|
||||
if(!wasFullscreen) {
|
||||
view.exitFullscreen();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1158,6 +1158,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
doc.body.querySelectorAll(`div.workspace-ribbon`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.mobile-navbar`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.status-bar`).forEach(node=>node.addClass(HIDE));
|
||||
doc.body.querySelectorAll(`div.titlebar`).forEach(node=>node.addClass(HIDE));
|
||||
}
|
||||
|
||||
hide(this.contentEl);
|
||||
|
||||
@@ -185,6 +185,7 @@ function RenderObsidianView(
|
||||
rootSplit.containerEl.style.width = '100%';
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
view.plugin.setDebounceActiveLeafChangeHandler();
|
||||
leafRef.current = {
|
||||
leaf: view.app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null,
|
||||
|
||||
@@ -494,7 +494,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
display: this.state.minimized ? "none" : "block",
|
||||
}}
|
||||
>
|
||||
<div className="panelColumn">
|
||||
<div className="selected-shape-actions">
|
||||
<fieldset>
|
||||
<legend>Utility actions</legend>
|
||||
<div className="buttonList buttonListIcon">
|
||||
|
||||
@@ -349,6 +349,10 @@ label.color-input-container > input {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.excalidraw .App-mobile-menu {
|
||||
width: 12.5rem !important;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn .buttonList {
|
||||
max-width: 13rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user