Reputation: 402
I have 2 lines that intersect at a point with know coordinates - x1,y1 - x2,y2 - x3,y3
From this I have calculated an arc at a given radius between the lines. So I now know - 2 arc endpoints x4,y4 and x5,y5 - arc centrepoint Cx,Cy - arc radius r - starting and ending angles relative to the X axis in polar and therefore the angle between the lines.
I want to create a formula that will calculate the maximum and minimum X and Y values of the arc. I.e. the coordinates of the box that would enclose the arc.
In the example below I can find out the minimum X value and maximum Y value, they are known values, but am unsure how to calculate the maximum X and minimum Y.
In other instances the arc could be any coordinates so the known minimum and maximum values will change.
I know how to calculate points along an arc at a given angle or intervals but not the maximum and minimum in a particular direction, in this case X and Y axis.
I am going to use the formula in programming.
Upvotes: 4
Views: 3824
Reputation: 1
There are so many issues when managing arcs, but @chris answer above made sense to me.
I've added a counter-clockwise argument to the bounds and corrected an error in one of the arrays. The function has minor fixes and has been battle tested in a CAD app. Works great.
Below is a clip from an Arc class with start and end angles (sa, ea) and ccw property. Start and end angles are simply reversed for ccw arcs.
bounds():Segment {
this.normalizeAngles();
const start = this.ccw ? this.ea : this.sa;
const end = this.ccw ? this.sa : this.ea;
const iniQuad = this.getQuadrant(start);
const endQuad = this.getQuadrant(end);
const r = this.r;
const ix = Math.cos(start) * this.r;
const iy = Math.sin(start) * this.r;
const ex = Math.cos(end) * this.r;
const ey = Math.sin(end) * this.r;
const minX = Math.min(ix, ex);
const minY = Math.min(iy, ey);
const maxX = Math.max(ix, ex);
const maxY = Math.max(iy, ey);
const xMax = [[maxX, r, r, r], [maxX, maxX, r, r], [maxX, maxX, maxX, r], [maxX, maxX, maxX, maxX]];
const yMax = [[maxY, maxY, maxY, maxY], [r, maxY, r, r], [r, maxY, maxY, r], [r, maxY, maxY, maxY]];
const xMin = [[minX, -r, minX, minX], [minX, minX, minX, minX], [-r, -r, minX, -r], [-r, -r, minX, minX]];
const yMin = [[minY, -r, -r, minY], [minY, minY, -r, minY], [minY, minY, minY, minY], [-r, -r, -r, minY]];
const x1 = xMin[endQuad][iniQuad];
const y1 = yMin[endQuad][iniQuad];
const x2 = xMax[endQuad][iniQuad];
const y2 = yMax[endQuad][iniQuad];
return new Segment(
new Point(this.c.x + x1, this.c.y + y1),
new Point(this.c.x + x2, this.c.y + y2)
);
}
private getQuadrant(angle:number) {
const PI = Math.PI;
const HALF_PI = Math.PI / 2;
const TWO_PI = Math.PI * 2;
if (angle > TWO_PI) angle = angle % TWO_PI;
if (angle >= 0 && angle < HALF_PI) return 0;
if (angle >= HALF_PI && angle < PI) return 1;
if (angle >= PI && angle < (PI + HALF_PI)) return 2;
return 3;
};
normalizeAngles() {
this.sa = this.normalizeAngle(this.sa);
this.ea = this.normalizeAngle(this.ea);
if (this.sa >= this.ea) this.ea += Math.PI * 2;
}
normalizeAngle(angle:number):number {
let remainder = angle % (2 * Math.PI);
if (remainder < 0) remainder += 2 * Math.PI;
return remainder;
}
Upvotes: 0
Reputation: 3193
To add another solution; I implemented the idea from https://groups.google.com/g/comp.graphics.algorithms/c/GtvMc05E0CQ/m/duaoXIWaqJIJ into the following example:
0,0
is at center of circlestartAngle === endAngle
it's ambiguous if arc is whole circle or nothing, by default method returns bounding box for whole circle.const svg = d3
.select("section")
.append("svg")
.attr('width', 200)
.attr('height', 200)
.append('g')
.attr('transform', d3.zoomIdentity.translate(100, 100).toString());
const pathShape = d3
.arc()
.startAngle(datum => degToRad(datum.startAngle))
.endAngle(datum => degToRad(datum.endAngle))
.innerRadius(datum => datum.radius)
.outerRadius(datum => datum.radius);
function mod(
n,
m
) {
return ((n % m) + m) % m;
}
function degToRad(
degree
) {
return degree * Math.PI / 180;
}
function reDraw() {
const radius = 100;
const startAngleInput = parseInt($('#start-angle').val());
const endAngleInput = parseInt($('#end-angle').val());
$('[data-field=start-angle]').text(startAngleInput);
$('[data-field=end-angle]').text(endAngleInput);
const startAngle = Math.min(startAngleInput, endAngleInput);
const endAngle = Math.max(startAngleInput, endAngleInput);
const cross0 = mod(startAngle, 360) >= mod(endAngle, 360);
const cross90 = mod(startAngle - 90, 360) >= mod(endAngle - 90, 360);
const cross180 = mod(startAngle - 180, 360) >= mod(endAngle - 180, 360);
const cross270 = mod(startAngle - 270, 360) >= mod(endAngle - 270, 360);
$('[data-field=cross-0]').text(cross0);
$('[data-field=cross-90]').text(cross90);
$('[data-field=cross-180]').text(cross180);
$('[data-field=cross-270]').text(cross270);
const startX = radius * Math.cos(degToRad(startAngle));
const startY = radius * Math.sin(degToRad(startAngle));
const endX = radius * Math.cos(degToRad(endAngle));
const endY = radius * Math.sin(degToRad(endAngle));
const right = cross0 ? +radius : Math.max(startX, endX);
const bottom = cross90 ? +radius : Math.max(startY, endY);
const left = cross180 ? -radius : Math.min(startX, endX);
const top = cross270 ? -radius : Math.min(startY, endY);
$('[data-field=right]').text(right.toFixed(2));
$('[data-field=top]').text(top.toFixed(2));
$('[data-field=left]').text(left.toFixed(2));
$('[data-field=bottom]').text(bottom.toFixed(2));
const pathSelectAll = svg
.selectAll('path')
.data([{
// input angles start at 3 o'clock
// SVG angle starts at 12 o'clock
startAngle: startAngle + 90,
endAngle: endAngle + 90,
radius: radius,
}]);
const pathEnter = pathSelectAll
.enter()
.append('path')
.attr('fill', 'none')
.attr('stroke', 'black');
pathSelectAll
.merge(pathEnter)
.attr('d', datum => pathShape(datum));
const circleSelectAll = svg
.selectAll('circle')
.data([{
cx: startX,
cy: startY,
}, {
cx: endX,
cy: endY,
}]);
const circleEnter = circleSelectAll
.enter()
.append('circle')
.attr('fill', 'none')
.attr('stroke', 'blue');
circleSelectAll
.merge(circleEnter)
.attr('r', 10)
.attr('cx', datum => datum.cx)
.attr('cy', datum => datum.cy);
const rectSelectAll = svg
.selectAll('rect')
.data([{
right: right,
top: top,
left: left,
bottom: bottom,
}]);
const rectEnter = rectSelectAll
.enter()
.append('rect')
.attr('fill', 'none')
.attr('stroke', 'red');
rectSelectAll
.merge(rectEnter)
.attr('x', datum => datum.left)
.attr('y', datum => datum.top)
.attr('width', datum => Math.abs(datum.left - datum.right))
.attr('height', datum => Math.abs(datum.top - datum.bottom));
}
reDraw();
$(document).on('input', '#start-angle', reDraw);
$(document).on('input', '#end-angle', reDraw);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.js" integrity="sha512-F63QPFxQ27mn9COmkZhSirC1pBNeVJ7MJJs4wtK6XfiAaH3WM1SfX6Sv2Pme/499+hafP0dALVZOADw4W2r6eQ==" crossorigin="anonymous"></script>
<table>
<tbody>
<tr>
<th>
<label for="start-angle">Start Angle</label>
<input id="start-angle" type="range" min="0" max="360" value="10" step="1">
</th>
<td data-field="start-angle"></td>
</tr>
<tr>
<th>
<label for="end-angle">End Angle</label>
<input id="end-angle" type="range" min="0" max="360" value="200" step="1">
</th>
<td data-field="end-angle"></td>
</tr>
</tbody>
</table>
<section></section>
<table>
<tbody>
<tr>
<th>Cross 0?</th>
<td data-field="cross-0"></td>
</tr>
<tr>
<th>Cross 90?</th>
<td data-field="cross-90"></td>
</tr>
<tr>
<th>Cross 180?</th>
<td data-field="cross-180"></td>
</tr>
<tr>
<th>Cross 270?</th>
<td data-field="cross-270"></td>
</tr>
<tr>
<th>Right</th>
<td data-field="right"></td>
</tr>
<tr>
<th>Top</th>
<td data-field="top"></td>
</tr>
<tr>
<th>Left</th>
<td data-field="left"></td>
</tr>
<tr>
<th>Bottom</th>
<td data-field="bottom"></td>
</tr>
</tbody>
</table>
I know this is old but the current accepted answer is really inefficient and not even accurate, it just brute forces the answer by trying a bunch of points along the arc...
imbrizi's JavaScript implementation seems to break down at certain points e.g.:
Upvotes: 3
Reputation: 3838
Oleg Petrochenko's answer implemented in Javascript:
const PI = Math.PI;
const HALF_PI = Math.PI / 2;
const TWO_PI = Math.PI * 2;
const DEG_TO_RAD = Math.PI / 180;
const RAD_TO_DEG = 180 / Math.PI;
const getQuadrant = (_angle) => {
const angle = _angle % (TWO_PI);
if (angle > 0.0 && angle < HALF_PI) return 0;
if (angle >= HALF_PI && angle < PI) return 1;
if (angle >= PI && angle < PI + HALF_PI) return 2;
return 3;
};
const getArcBoundingBox = (ini, end, radius, margin = 0) => {
const iniQuad = getQuadrant(ini);
const endQuad = getQuadrant(end);
const ix = Math.cos(ini) * radius;
const iy = Math.sin(ini) * radius;
const ex = Math.cos(end) * radius;
const ey = Math.sin(end) * radius;
const minX = Math.min(ix, ex);
const minY = Math.min(iy, ey);
const maxX = Math.max(ix, ex);
const maxY = Math.max(iy, ey);
const r = radius;
const xMax = [[maxX, r, r, r], [maxX, maxX, r, r], [maxX, maxX, maxX, r], [maxX, maxX, maxX, maxX]];
const yMax = [[maxY, maxY, maxY, maxY], [r, maxY, r, r], [r, maxY, maxY, r], [r, maxY, maxY, maxY]];
const xMin = [[minX, -r, minX, minX], [minX, minX, minX, minX], [-r, -r, minX, -r], [-r, -r, minX, minX]];
const yMin = [[minY, -r, -r, minY], [minY, minY, -r, minY], [minY, minY, minY, minY], [-r, -r, -r, minY]];
const x1 = xMin[endQuad][iniQuad];
const y1 = yMin[endQuad][iniQuad];
const x2 = xMax[endQuad][iniQuad];
const y2 = yMax[endQuad][iniQuad];
const x = x1 - margin;
const y = y1 - margin;
const w = x2 - x1 + margin * 2;
const h = y2 - y1 + margin * 2;
return { x, y, w, h };
};
Here is a jsfiddle: https://jsfiddle.net/brunoimbrizi/y3to5s6n/45/
Upvotes: 2
Reputation: 61
Suppose we have start angle θ1, end angle θ2 (both in radians), radus r, direction of the arc counterclockwise. We'd like to find Xmax,Ymax,Xmin and Ymin. Consider this values as functions of quadrants q=f(θ):
Xmax=f(q1,q2,r), Ymax=f(q1,q2,r), Xmin=f(q1,q2,r), Ymin=f(q1,q2,r).
Instead of writing huge number of "if" statements it's convenient to represent this functions as an "extremum matrices". Evaluating functions f(q1,q2,r) we'll end up with this matrices.
So here is the algorithm:
Here's my C#6 implementation:
using System;
using System.Windows;
using static System.Math;
public static class GeomTools
{
public static Byte GetQuadrant(this Double angle)
{
var trueAngle = angle%(2*PI);
if (trueAngle >= 0.0 && trueAngle < PI/2.0)
return 1;
if (trueAngle >= PI/2.0 && trueAngle < PI)
return 2;
if (trueAngle >= PI && trueAngle < PI*3.0/2.0)
return 3;
if (trueAngle >= PI*3.0/2.0 && trueAngle < PI*2)
return 4;
return 0;
}
public static Rect GetBounds(Double startAngle, Double endAngle, Double r)
{
var startQuad = startAngle.GetQuadrant() - 1;
var endQuad = endAngle.GetQuadrant() - 1;
// Convert to Cartesian coordinates.
var stPt = new Point(Round(r*Cos(startAngle), 14), Round(r*Sin(startAngle), 14));
var enPt = new Point(Round(r*Cos(endAngle), 14), Round(r*Sin(endAngle), 14));
// Find bounding box excluding extremum.
var minX = stPt.X;
var minY = stPt.Y;
var maxX = stPt.X;
var maxY = stPt.Y;
if (maxX < enPt.X) maxX = enPt.X;
if (maxY < enPt.Y) maxY = enPt.Y;
if (minX > enPt.X) minX = enPt.X;
if (minY > enPt.Y) minY = enPt.Y;
// Build extremum matrices.
var xMax = new[,] {{maxX, r, r, r}, {maxX, maxX, r, r}, {maxX, maxX, maxX, r}, {maxX, maxX, maxX, maxX}};
var yMax = new[,] {{maxY, maxY, maxY, maxY}, {r, maxY, r, r}, {r, maxY, maxY, r}, {r, maxY, maxY, maxY}};
var xMin = new[,] {{minX, -r, minX, minX}, {minX, minX, minX, minX}, {-r, -r, minX, -r}, {-r, -r, minX, minX}};
var yMin = new[,] {{minY, -r, -r, minY}, {minY, minY, -r, minY}, {minY, minY, minY, minY}, {-r, -r, -r, minY}};
// Select desired values
var startPt =new Point(xMin[endQuad, startQuad], yMin[endQuad, startQuad]);
var endPt=new Point(xMax[endQuad, startQuad], yMax[endQuad, startQuad]);
return new Rect(startPt,endPt);
}
}
It is fair for arc centre point at (0,0) but you can easily move resulting bounding box to your Cx,Cy.
Unlike Tim Buegeleisen's approximate solution this solution is exact, though it may be a little bit more memory expensive.
Upvotes: 6
Reputation: 521289
I have an algorithmic solution you can try using. It involves scanning the polar coordinate space in between your known starting and ending points on the arc, and keeping track of the minimum and maximum values.
Here are the basic steps of the algorithm:
I took advantage of the following two equations to convert polar to Cartedian coordinates:
x = r*cosθ
y = r*sinθ
Here is an equation to convert Cartesian coordinates to a polar angle:
θ = tan-1(y / x)
You need to watch out for the potential divide by zero in this equation. Arc tangent of infinity is Pi / 2
radians.
This solution assumes that an arc begins and traverses counter-clockwise from a low radian value to a high radian value.
// Input Parameters:
// (x1, y1) first point on arc
// (x2, y2) second point on arc
// (xc, yc) center point of circle
public void findMinMax(double x1, double x2, double y1, double y2, double xc, double yc) {
double xMin, yMin, xMax, yMax;
// compute radius of circle
double radius = Math.sqrt(Math.pow((xc - x1), 2) + Math.pow((yc - y1), 2));
// compute starting and ending points in polar coordinates
double t1 = 0.0;
if (x1 == 0.0) {
t1 = Math.PI / 2;
}
else {
t1 = Math.atan(y1 / x1);
}
double t2 = 0.0;
if (x2 == 0.0) {
t2 = Math.PI / 2;
}
else {
t2 = Math.atan(y2 / x2);
}
// determine starting and ending polar angles
double tStart, tEnd;
if (t1 < t2) {
tStart = t1;
tEnd = t2;
}
else {
tStart = t2;
tEnd = t1;
}
// now scan the polar space at fixed radius and find
// the minimum AND maximum Cartesian x and y values
double delta = 0.01;
// initialize min and max coordinates to first point
xMin = radius * Math.cos(tStart);
yMin = radius * Math.sin(tStart);
xMax = xMin;
yMax = yMin;
for (double theta=tStart; theta < tEnd; theta += delta) {
// compute coordinates
double x = radius * Math.cos(theta);
double y = radius * Math.sin(theta);
if (x > xMax) {
xMax = x;
}
if (x < xMin) {
xMin = x;
}
if (y > yMax) {
yMax = y;
}
if (y < yMin) {
yMin = y;
}
}
// display min and max values
System.out.println("xMin = " + xMin + ", yMin = " + yMin);
System.out.println("xMax = " + xMax + ", yMax = " + yMax);
}
Testing
Arc starting at (5, 0) and ending at (0, 5) with center point (0, 0)
findMinMax(5, 0, 0, 5, 0, 0)
xMin = 0.003981633553660766, yMin = 0.0
xMax = 5.0, yMax = 4.999998414659173
Upvotes: 2
Reputation: 72312
First find in which quadrant the endpoints are.
If they are in the same quadrant, then the arc is monotonic and the bounding box is easy.
Otherwise, each time you cross a quadrant, you'll get an extreme point that is an endpoint of a horizontal or vertical diameter.
Not too complicated to write an algorithm for that, though there may be several cases to consider, including the orientation of the arc.
Upvotes: 1