diff --git a/ea-scripts/README.md b/ea-scripts/README.md index bd13f07..2ee0fe9 100644 --- a/ea-scripts/README.md +++ b/ea-scripts/README.md @@ -71,6 +71,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi |[Set stroke width of selected elements](Set%20Stroke%20Width%20of%20Selected%20Elements.md)|This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.||[@zsviczian](https://github.com/zsviczian)| |[Split text by lines](Split%20text%20by%20lines.md)|Split lines of text into separate text elements for easier reorganization||[@zsviczian](https://github.com/zsviczian)| |[Set Text Alignment](Set%20Text%20Alignment.md)|Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.||[@zsviczian](https://github.com/zsviczian)| +|[Split Ellipse](Split%20Ellipse.md)|This script splits an ellipse at any point where a line intersects it.||[@GColoy](https://github.com/GColoy)| |[TheBrain-navigation](TheBrain-navigation.md)|An Excalidraw based graph user interface for your Vault. Requires the [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview). Generates a graph view similar to that of [TheBrain](https://TheBrain.com) plex. Watch introduction to this script on [YouTube](https://youtu.be/plYobK-VufM).||[@zsviczian](https://github.com/zsviczian)| |[Toggle Fullscreen on Mobile](Toggle%20Fullscreen%20on%20Mobile.md)|Hides Obsidian workspace leaf padding and header (based on option in settings, default is "hide header" = false) which will take Excalidraw to full screen. ⚠ Note that if the header is not visible, it will be very difficult to invoke the command palette to end full screen. Only hide the header if you have a keyboard or you've practiced opening command palette!||[@zsviczian](https://github.com/zsviczian)| |[Toggle Grid](Toggle%20Grid.md)|Toggles the grid.||[@GColoy](https://github.com/GColoy)| diff --git a/ea-scripts/Split Ellipse.md b/ea-scripts/Split Ellipse.md new file mode 100644 index 0000000..76bc4ff --- /dev/null +++ b/ea-scripts/Split Ellipse.md @@ -0,0 +1,208 @@ +/* + +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. + + +See documentation for more details: +https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html + +```javascript +*/ +const elements = ea.getViewSelectedElements(); +const ellipse = elements.filter(el => el.type == "ellipse")[0]; +if (!ellipse) return; + +let lines = elements.filter(el => el.type == "line" || el.type == "arrow"); +if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow"); +const subLines = getSubLines(lines); + +const angles = subLines.flatMap(line => { + return intersectionAngleOfEllipseAndLine(ellipse, line.a, line.b).map(result => ({ + angle: result, + cuttingLine: line + })); +}); + +if (angles.length === 0) angles.push({ angle: 0, cuttingLine: null }); + +angles.sort((a, b) => a.angle - b.angle); + +const closeObject = await utils.suggester(["Yes", "No"], [true, false], "Close object along cutedge?") + +ea.style.strokeSharpness = closeObject ? "sharp" : "round"; +ea.style.strokeColor = ellipse.strokeColor; +ea.style.strokeWidth = ellipse.strokeWidth; +ea.style.backgroundColor = ellipse.backgroundColor; +ea.style.fillStyle = ellipse.fillStyle; +ea.style.roughness = ellipse.roughness; + +angles.forEach((angle, key) => { + const cuttingLine = angle.cuttingLine; + angle = angle.angle; + const nextAngleKey = (key + 1) < angles.length ? key + 1 : 0; + const nextAngle = angles[nextAngleKey].angle; + const AngleDelta = nextAngle - angle ? nextAngle - angle : Math.PI*2; + const pointAmount = Math.ceil((AngleDelta*64)/(Math.PI*2)); + const stepSize = AngleDelta/pointAmount; + let points = drawEllipse(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.angle, angle, nextAngle, stepSize); + if (closeObject && cuttingLine) points = points.concat(getCutLine(points[0], angles[key], angles[nextAngleKey], ellipse)); + + const lineId = ea.addLine(points); + const line = ea.getElement(lineId); + line.frameId = ellipse.frameId; + line.groupIds = ellipse.groupIds; +}); + +ea.deleteViewElements([ellipse]); +ea.addElementsToView(false,false,true); +return; + +function getSubLines(lines) { + return lines.flatMap((line, key) => { + return line.points.slice(1).map((pointB, i) => ({ + a: addVectors([line.points[i], [line.x, line.y]]), + b: addVectors([pointB, [line.x, line.y]]), + originLineIndex: key, + indexPointA: i, + })); + }); +} + +function intersectionAngleOfEllipseAndLine(ellipse, pointA, pointB) { + /* + To understand the code in this function and subfunctions it might help to take a look at this geogebra file + https://www.geogebra.org/m/apbm3hs6 + */ + const c = multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2)); + const a = rotateVector( + addVectors([ + pointA, + invVec([ellipse.x, ellipse.y]), + invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2))) + ]), + -ellipse.angle + ) + const l_b = rotateVector( + addVectors([ + pointB, + invVec([ellipse.x, ellipse.y]), + invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2))) + ]), + -ellipse.angle + ); + const b = addVectors([ + l_b, + invVec(a) + ]); + const solutions = calculateLineSegment(a[0], a[1], b[0], b[1], c[0], c[1]); + return solutions + .filter(num => isBetween(num, 0, 1)) + .map(num => { + const point = [ + (a[0] + b[0] * num) / ellipse.width, + (a[1] + b[1] * num) / ellipse.height + ]; + return angleBetweenVectors([1, 0], point); + }); +} + +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 getCutLine(startpoint, currentAngle, nextAngle, ellipse) { + if (currentAngle.cuttingLine.originLineIndex != nextAngle.cuttingLine.originLineIndex) return []; + + const originLineIndex = currentAngle.cuttingLine.originLineIndex; + + if (lines[originLineIndex] == 2) return startpoint; + + const originLine = []; + lines[originLineIndex].points.forEach(p => originLine.push(addVectors([ + p, + [lines[originLineIndex].x, lines[originLineIndex].y] + ]))); + + const edgepoints = []; + const direction = isInEllipse(originLine[clamp(nextAngle.cuttingLine.indexPointA - 1, 0, originLine.length - 1)], ellipse) ? -1 : 1 + let i = isInEllipse(originLine[nextAngle.cuttingLine.indexPointA], ellipse) ? nextAngle.cuttingLine.indexPointA : nextAngle.cuttingLine.indexPointA + direction; + while (isInEllipse(originLine[i], ellipse)) { + edgepoints.push(originLine[i]); + i = (i + direction) % originLine.length; + } + edgepoints.push(startpoint); + return edgepoints; +} + +function calculateLineSegment(ax, ay, bx, by, cx, cy) { + const sqrt = Math.sqrt((cx ** 2) * (cy ** 2) * (-(ay ** 2) * (bx ** 2) + 2 * ax * ay * bx * by - (ax ** 2) * (by ** 2) + (bx ** 2) * (cy ** 2) + (by ** 2) * (cx ** 2))); + const numerator = -(ay * by * (cx ** 2) + ax * bx * (cy ** 2)); + const denominator = ((by ** 2) * (cx ** 2) + (bx ** 2) * (cy ** 2)); + const t1 = (numerator + sqrt) / denominator; + const t2 = (numerator - sqrt) / denominator; + + return [t1, t2]; +} + +function isInEllipse(point, ellipse) { + point = addVectors([point, invVec([ellipse.x, ellipse.y]), invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], 1/2))]); + point = [point[0]*2/ellipse.width, point[1]*2/ellipse.height]; + const distance = Math.sqrt(point[0]**2 + point[1]**2); + return distance < 1; +} + +function angleBetweenVectors(v1, v2) { + let dotProduct = v1[0] * v2[0] + v1[1] * v2[1]; + let determinant = v1[0] * v2[1] - v1[1] * v2[0]; + let angle = Math.atan2(determinant, dotProduct); + return angle < 0 ? angle + 2 * Math.PI : angle; +} + +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]); +} + +function invVec(vector) { + return [-vector[0], -vector[1]]; +} + +function multiplyVectorByScalar(vector, scalar) { + return [vector[0] * scalar, vector[1] * scalar]; +} + +function round(number, precision) { + var factor = Math.pow(10, precision); + return Math.round(number * factor) / factor; +} + +function isBetween(num, min, max) { + return (num >= min && num <= max); +} + +function clamp(number, min, max) { + return Math.max(min, Math.min(number, max)); +} diff --git a/ea-scripts/Split Ellipse.svg b/ea-scripts/Split Ellipse.svg new file mode 100644 index 0000000..3ccb624 --- /dev/null +++ b/ea-scripts/Split Ellipse.svg @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/ea-scripts/index-new.md b/ea-scripts/index-new.md index 67c8f3c..1cdb703 100644 --- a/ea-scripts/index-new.md +++ b/ea-scripts/index-new.md @@ -120,6 +120,7 @@ I would love to include your contribution in the script library. If you have a s |
| Author | @zsviczian |
| Source | File on GitHub |
| Description | The script will convert your drawing into a slideshow presentation. |
| Author | @GColoy |
| Source | File on GitHub |
| Description | 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. |