The method returns the Cartesian axis aligned bounding box.
Not the ellipse axis aligned bounding box ... That is straight forward.
A note in this method states:
Code: Select all
// kind of a brute force. TODO: exact calculationUntil the parameter angle is no longer between both ends or with an extreme break limit of 4Pi or larger.
For each tested point on the ellipse it figures out if it is outside the box lower or higher limits in X or Y and adapts some limits if necessary.
It should not be that hard to figure out where the gradient dx/dt and dy/dt are zero.
Or simpler said: Where the slope is purely vertical or purely horizontal.
That are 4 parameter angles and their respective positions to be included if those are on the ellipse.
Instead of testing up to 210 positions or more.
Another advantage is that this is quite accurate, because with a step size of 0.03rad the local extreme value might be missed.
A search online confirmed my suspicion, there is even an easier method for a full ellipse.
Below is a JS-script analog for replacing REllipse::getBoundingBox ... To be converted to C++, sorry.
It uses the easier method for a full ellipse ... Or when start ≈/= end ... Or when more than a full turn.
And falls back on the little more complicated method for a limited ellipse arc.
Online references are included.
Code: Select all
/**
* Cartesian axis aligned bounding box of this ellipse.
* For an ellipse axis aligned bounding box see: REllipse::getBoxCorners().
* \author Original by Andrew Mustun, near exact calculation by CVH (c) 2025.
*
* \return RBox object, at least an invalid RBox.
*/
function REllipseJS_getBoundingBox(ellipse) {
// JS-script analog for replacing REllipse::getBoundingBox(), to be converted to C++
// Original used brute force with a coordinate test every t ++0.03rad (1.72° steps)
// Here the ellipse is a parameter, in C++ it is the RShape object itself and therefor not validated
// Ellipse radii, orientation and center:
var major = ellipse.getMajorRadius(); // Positive (Magnitude), typically oriented on the positive ellipse X-axis
var minor = ellipse.getMinorRadius(); // Positive, typically smaller than major or equal (=circular)
var ori = ellipse.getAngle(); // Angle towards the primary major point relative to the center
var center = ellipse.getCenter();
// Bounding box:
var bbox;
// Simplified method for a full ellipse:
// Including start ≈/= end and more than a full turn
// Based on a contribution by user1789690 Jan 4, 2013 at 19:15
// https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-bbox-of-an-ellipse
// Based on a publication by Inigo Quilez: working with ellipses - 2006
// https://iquilezles.org/articles/ellipses/
if (ellipse.isFullEllipse()) {
var ux = major * Math.cos(ori);
var uy = major * Math.sin(ori);
var vx = minor * Math.cos(ori + Math.PI/2);
var vy = minor * Math.sin(ori + Math.PI/2);
// Define bounding box by center, width and height:
bbox = new RBox(center, 2.0 * Math.sqrt(ux*ux + vx*vx), 2.0 * Math.sqrt(uy*uy + vy*vy));
}
// Not a full ellipse:
// Based on a contribution by Mike Tunnicliffe Sep 17, 2008 at 21:47
// https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-bbox-of-an-ellipse
else {
// Ellipse parameter angles, reversed flag and ratio:
var pa1 = ellipse.getStartParam();
var pa2 = ellipse.getEndParam();
var rev = ellipse.isReversed();
var ratio = ellipse.getRatio(); // = minor/major. Note that this is an REllipse Public Attribute itself
// Center coordinates:
var cx = center.getX();
var cy = center.getY();
var t, x, y;
// Include ellipse start and end point:
var sp = ellipse.getStartPoint();
bbox = new RBox(sp, sp);
bbox.growToIncludePoint(ellipse.getEndPoint());
// Conditionally include extremes where the gradient dx/dt turns zero:
// # Not an issue # in a JS-script
// tan(Pi/2±nPi) should result in an error but is ±16331239353195370
// Also doesn't fail for: tan(Pi/2+2ULP), tan(Pi/2+1ULP), tan(Pi/2-1ULP), tan(Pi/2-2ULP)
// atan(±huge) results in ±Pi/2
t = Math.atan(-ratio * Math.tan(ori));
if (RMath.isAngleBetween(t, pa1, pa2, rev)) { // Angles handled as normalized: (0 <= a < 2Pi)
x = cx + (major * Math.cos(t) * Math.cos(ori)) - (minor * Math.sin(t) * Math.sin(ori));
bbox.growToIncludePoint(new RVector(x, sp.getY()));
}
// Opposite extreme:
t += Math.PI;
if (RMath.isAngleBetween(t, pa1, pa2, rev)) { // Angles handled as normalized: (0 <= a < 2Pi)
x = cx + (major * Math.cos(t) * Math.cos(ori)) - (minor * Math.sin(t) * Math.sin(ori));
bbox.growToIncludePoint(new RVector(x, sp.getY()));
}
// Conditionally include extremes where the gradient dy/dt turns zero:
// # Not an issue # in a JS-script
// tan(Pi/2±nPi) should result in an error but is ±16331239353195370
// atan(near ±zero) results in near ±zero
t = Math.atan(ratio / Math.tan(ori));
if (RMath.isAngleBetween(t, pa1, pa2, rev)) { // Angles handled as normalized: (0 <= a < 2Pi)
y = cy + (major * Math.cos(t) * Math.sin(ori)) + (minor * Math.sin(t) * Math.cos(ori));
bbox.growToIncludePoint(new RVector(sp.getX(), y));
}
// Opposite extreme:
t += Math.PI;
if (RMath.isAngleBetween(t, pa1, pa2, rev)) { // Angles handled as normalized: (0 <= a < 2Pi)
y = cy + (major * Math.cos(t) * Math.sin(ori)) + (minor * Math.sin(t) * Math.cos(ori));
bbox.growToIncludePoint(new RVector(sp.getX(), y));
}
}
// # Informative # Remove in final:
var corners = bbox.getCorners();
EAction.handleUserInfo("getBoundingBox: ori: %1 tan(): %2".arg(RMath.rad2deg(ori).toString()).arg(Math.tan(ori).toString()));
EAction.handleUserMessage("%1; %2".arg(corners[0].x.toString().replace(".", ",")).arg(corners[0].y.toString().replace(".", ",")));
EAction.handleUserMessage("%1; %2".arg(corners[1].x.toString().replace(".", ",")).arg(corners[1].y.toString().replace(".", ",")));
EAction.handleUserMessage("%1; %2".arg(corners[2].x.toString().replace(".", ",")).arg(corners[2].y.toString().replace(".", ",")));
EAction.handleUserMessage("%1; %2".arg(corners[3].x.toString().replace(".", ",")).arg(corners[3].y.toString().replace(".", ",")));
// # Informative # Remove in final
// Return generated bounding box:
return bbox;
};
The messages in the command history let me copy/paste coordinates to use as input for LI and then set closed.
For me in the comma-semicolon notation.
See # Informative # part, to be removed in final code.
Endpoints of a not full ellipse arc are included as they are, nothing to gain there.
With another optimized ellipse method getTangentPoint(line) I can verify the tangent nature for a selected ellipse and a possible tangent line.
This is able to handle secant (near tangent), true tangent but also near tangent exterior lines with extreme accuracy. (See topic 11900)
For the edges of the returned bounding box:
When not exact the differences are in the last or last 2 significant digits of the floating point representation.
It doesn't really degrade with size or location.
Conclusion: Near exact or 'As good as it gets'.
Continuing my research ...
At some point we can get rid of the annoying unnecessary larger tolerances for ellipse related things.
Regards,
CVH