plotboilerplate
Version:
A simple javascript plotting boilerplate for 2d stuff.
428 lines • 21.8 kB
JavaScript
"use strict";
/**
* Implementation of elliptic sectors.
* Note that sectors are constructed in clockwise direction.
*
* @author Ikaros Kappler
* @date 2021-02-26
* @modified 2022-02-02 Added the `destroy` method.
* @modified 2022-11-01 Tweaked the `endpointToCenterParameters` function to handle negative values, too, without errors.
* @modified 2025-04-01 Adapting a the `toCubicBezier` calculation to match an underlying change in the vertAt and tangentAt calculation of ellipses (was required to hamonize both methods with circles).
* @modified 2025-04-02 Adding `VEllipseSector.containsAngle` method.
* @modified 2025-04-02 Adding `VEllipseSector.lineIntersections` and `VEllipseSector.lineIntersectionTangents` and implementing `Intersectable`.
* @modified 2025-04-07 Adding value wrapping (0 to TWO_PI) to the `VEllipseSector.containsAngle` method.
* @modified 2025-04-09 Adding the `VEllipseSector.move` method.
* @modified 2025-04-19 Added the `VEllipseSector.getStartPoint` and `getEndPoint` methods.
* @modified 2025-04-23 Added the `VEllipseSector.getBounds` method.
* @version 1.2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VEllipseSector = void 0;
var Bounds_1 = require("./Bounds");
var CubicBezierCurve_1 = require("./CubicBezierCurve");
var geomutils_1 = require("./geomutils");
var Line_1 = require("./Line");
var UIDGenerator_1 = require("./UIDGenerator");
var VEllipse_1 = require("./VEllipse");
var Vertex_1 = require("./Vertex");
/**
* @classdesc A class for elliptic sectors.
*
* @requires Line
* @requires Vector
* @requires VertTuple
* @requires Vertex
* @requires SVGSerializale
* @requires UID
* @requires UIDGenerator
**/
var VEllipseSector = /** @class */ (function () {
/**
* Create a new elliptic sector from the given ellipse and two angles.
*
* Note that the direction from start to end goes clockwise, and that start and end angle
* will be wrapped to [0,PI*2).
*
* @constructor
* @name VEllipseSector
* @param {VEllipse} - The underlying ellipse to use.
* @param {number} startAngle - The start angle of the sector.
* @param {numner} endAngle - The end angle of the sector.
*/
function VEllipseSector(ellipse, startAngle, endAngle) {
/**
* Required to generate proper CSS classes and other class related IDs.
**/
this.className = "VEllipseSector";
this.uid = UIDGenerator_1.UIDGenerator.next();
this.ellipse = ellipse;
this.startAngle = geomutils_1.geomutils.wrapMinMax(startAngle, 0, Math.PI * 2);
this.endAngle = geomutils_1.geomutils.wrapMinMax(endAngle, 0, Math.PI * 2);
}
/**
* Move the ellipse sector by the given amount.
*
* @method move
* @param {XYCoords} amount - The amount to move.
* @instance
* @memberof VEllipseSector
* @return {VEllipseSector} this for chaining
**/
VEllipseSector.prototype.move = function (amount) {
this.ellipse.move(amount);
return this;
};
/**
* Checks wether the given angle (must be inside 0 and PI*2) is contained inside this sector.
*
* @param {number} angle - The numeric angle to check.
* @method containsAngle
* @instance
* @memberof VEllipseSectpr
* @return {boolean} True if (and only if) this sector contains the given angle.
*/
VEllipseSector.prototype.containsAngle = function (angle) {
angle = geomutils_1.geomutils.mapAngleTo2PI(angle); // wrapMinMax(angle, 0, Math.PI * 2);
var sAngle = geomutils_1.geomutils.mapAngleTo2PI(this.startAngle);
var eAngle = geomutils_1.geomutils.mapAngleTo2PI(this.endAngle);
// TODO: cleanup
// if (this.startAngle <= this.endAngle) {
// return angle >= this.startAngle && angle < this.endAngle;
// } else {
// // startAngle > endAngle
// return angle >= this.startAngle || angle < this.endAngle;
// }
if (sAngle <= eAngle) {
return angle >= sAngle && angle < eAngle;
}
else {
// startAngle > endAngle
return angle >= sAngle || angle < eAngle;
}
};
/**
* Get the sectors starting point (on the underlying ellipse, located at the start angle).
*
* @method getStartPoint
* @instance
* @memberof VEllipseSector
* @return {Vertex} The sector's stating point.
*/
VEllipseSector.prototype.getStartPoint = function () {
return this.ellipse.vertAt(this.startAngle);
};
/**
* Get the sectors ending point (on the underlying ellipse, located at the end angle).
*
* @method getEndPoint
* @instance
* @memberof VEllipseSector
* @return {Vertex} The sector's ending point.
*/
VEllipseSector.prototype.getEndPoint = function () {
return this.ellipse.vertAt(this.endAngle);
};
//--- BEGIN --- Implement interface `IBounded`
/**
* Get the bounds of this elliptic sector.
*
* The bounds are approximated by the underlying segment buffer; the more segment there are,
* the more accurate will be the returned bounds.
*
* @method getBounds
* @instance
* @memberof VEllipse
* @return {Bounds} The bounds of this elliptic sector.
**/
VEllipseSector.prototype.getBounds = function () {
var _this = this;
// Calculage angles from east, west, north and south box points and check if they are inside
var extremes = this.ellipse.getExtremePoints();
var candidates = extremes.filter(function (point) {
var angle = new Line_1.Line(_this.ellipse.center, point).angle() - _this.ellipse.rotation;
return _this.containsAngle(angle);
});
return Bounds_1.Bounds.computeFromVertices([this.getStartPoint(), this.getEndPoint()].concat(candidates));
};
//--- BEGIN --- Implement interface `Intersectable`
/**
* Get the line intersections as vectors with this ellipse.
*
* @method lineIntersections
* @instance
* @memberof VEllipseSectpr
* @param {VertTuple<Vector>} ray - The line/ray to intersect this ellipse with.
* @param {boolean} inVectorBoundsOnly - (default=false) Set to true if only intersections within the vector bounds are of interest.
* @returns
*/
VEllipseSector.prototype.lineIntersections = function (ray, inVectorBoundsOnly) {
var _this = this;
if (inVectorBoundsOnly === void 0) { inVectorBoundsOnly = false; }
// First get all line intersections from underlying ellipse.
var ellipseIntersections = this.ellipse.lineIntersections(ray, inVectorBoundsOnly);
// Drop all intersection points that are not contained in the circle sectors bounds.
var tmpLine = new Line_1.Line(this.ellipse.center, new Vertex_1.Vertex());
return ellipseIntersections.filter(function (intersectionPoint) {
tmpLine.b.set(intersectionPoint);
var lineAngle = tmpLine.angle();
return _this.containsAngle(lineAngle - _this.ellipse.rotation);
});
};
/**
* Get all line intersections of this polygon and their tangents along the shape.
*
* This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
*
* @method lineIntersections
* @memberof VEllipseSectpr
* @param line
* @param lineIntersectionTangents
* @returns
*/
VEllipseSector.prototype.lineIntersectionTangents = function (line, inVectorBoundsOnly) {
var _this = this;
if (inVectorBoundsOnly === void 0) { inVectorBoundsOnly = false; }
// Find the intersections of all lines plus their tangents inside the circle bounds
var interSectionPoints = this.lineIntersections(line, inVectorBoundsOnly);
return interSectionPoints.map(function (vert) {
// Calculate angle
var lineFromCenter = new Line_1.Line(_this.ellipse.center, vert);
var angle = lineFromCenter.angle();
// console.log("angle", (angle / Math.PI) * 180.0);
// const angle = Math.random() * Math.PI * 2; // TODO
// Calculate tangent at angle
return _this.ellipse.tangentAt(angle);
});
};
//--- END --- Implement interface `Intersectable`
/**
* Convert this elliptic sector into cubic Bézier curves.
*
* @param {number=3} quarterSegmentCount - The number of segments per base elliptic quarter (default is 3, min is 1).
* @param {number=0.666666} threshold - The Bézier threshold (default value 0.666666 approximates the ellipse with best results
* but you might wish to use other values)
* @return {Array<CubicBezierCurve>} An array of cubic Bézier curves representing the elliptic sector.
*/
VEllipseSector.prototype.toCubicBezier = function (quarterSegmentCount, threshold) {
// There are at least 4 segments required (dour quarters) to approximate a whole
// ellipse with Bézier curves.
// A visually 'good' approximation should have 12; this seems to be a good value (anything multiple of 4).
var segmentCount = Math.max(1, quarterSegmentCount || 3) * 4;
threshold = typeof threshold === "undefined" ? 0.666666 : threshold;
var radiusH = this.ellipse.radiusH();
var radiusV = this.ellipse.radiusV();
var startAngle = VEllipseSector.ellipseSectorUtils.normalizeAngle(this.startAngle);
var endAngle = VEllipseSector.ellipseSectorUtils.normalizeAngle(this.endAngle);
// Find all angles inside start and end
var angles = VEllipseSector.ellipseSectorUtils.equidistantVertAngles(radiusH, radiusV, startAngle, endAngle, segmentCount);
angles = [startAngle].concat(angles).concat([endAngle]);
var curves = [];
var curAngle = angles[0];
var startPoint = this.ellipse.vertAt(curAngle);
for (var i = 0; i + 1 < angles.length; i++) {
var nextAngle = angles[(i + 1) % angles.length];
var endPoint = this.ellipse.vertAt(nextAngle);
var startTangent = this.ellipse.tangentAt(curAngle + this.ellipse.rotation);
var endTangent = this.ellipse.tangentAt(nextAngle + this.ellipse.rotation);
// Distorted ellipses can only be approximated by linear Bézier segments
if (Math.abs(radiusV) < 0.0001 || Math.abs(radiusH) < 0.0001) {
var diff = startPoint.difference(endPoint);
var curve = new CubicBezierCurve_1.CubicBezierCurve(startPoint.clone(), endPoint.clone(), startPoint.clone().addXY(diff.x * 0.333, diff.y * 0.333), endPoint.clone().addXY(-diff.x * 0.333, -diff.y * 0.333));
curves.push(curve);
}
else {
// Find intersection
var intersection = startTangent.intersection(endTangent);
// What if intersection is undefined?
// --> This *can* not happen if segmentCount > 2 and height and width of the ellipse are not zero.
if (intersection) {
// It's VERY LIKELY hat this ALWAYS happens; it's just a typesave variant.
// Intersection cannot be null.
var startDiff = startPoint.difference(intersection);
var endDiff = endPoint.difference(intersection);
var curve = new CubicBezierCurve_1.CubicBezierCurve(startPoint.clone(), endPoint.clone(), startPoint.clone().add(startDiff.scale(threshold)), endPoint.clone().add(endDiff.scale(threshold)));
curves.push(curve);
}
}
startPoint = endPoint;
curAngle = nextAngle;
}
return curves;
};
/**
* This function should invalidate any installed listeners and invalidate this object.
* After calling this function the object might not hold valid data any more and
* should not be used.
*/
VEllipseSector.prototype.destroy = function () {
this.ellipse.destroy();
this.isDestroyed = true;
};
VEllipseSector.ellipseSectorUtils = {
/**
* Helper function to convert an elliptic section to SVG arc params (for the `d` attribute).
* Inspiration found at:
* https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
*
* @param {boolean} options.moveToStart - If false (default=true) the initial 'Move' command will not be used.
* @return [ 'A', radiusH, radiusV, rotation, largeArcFlag=1|0, sweepFlag=0, endx, endy ]
*/
describeSVGArc: function (x, y, radiusH, radiusV, startAngle, endAngle, rotation, options) {
if (typeof options === "undefined")
options = { moveToStart: true };
if (typeof rotation === "undefined")
rotation = 0.0;
// Important note: this function only works if start- and end-angle are within
// one whole circle [x,x+2*PI].
// Revelations of more than 2*PI might result in unexpected arcs.
// -> Use the geomutils.wrapMax( angle, 2*PI )
startAngle = geomutils_1.geomutils.wrapMax(startAngle, Math.PI * 2);
endAngle = geomutils_1.geomutils.wrapMax(endAngle, Math.PI * 2);
// Find the start- and end-point on the rotated ellipse
// XYCoords to Vertex (for rotation)
var end = new Vertex_1.Vertex(VEllipse_1.VEllipse.utils.polarToCartesian(x, y, radiusH, radiusV, endAngle));
var start = new Vertex_1.Vertex(VEllipse_1.VEllipse.utils.polarToCartesian(x, y, radiusH, radiusV, startAngle));
end.rotate(rotation, { x: x, y: y });
start.rotate(rotation, { x: x, y: y });
// Boolean stored as integers (0|1).
var diff = endAngle - startAngle;
var largeArcFlag;
if (diff < 0) {
largeArcFlag = Math.abs(diff) < Math.PI ? 1 : 0;
}
else {
largeArcFlag = Math.abs(diff) > Math.PI ? 1 : 0;
}
var sweepFlag = 1;
var pathData = [];
if (options.moveToStart) {
pathData.push("M", start.x, start.y);
}
// Arc rotation in degrees, not radians.
var r2d = 180 / Math.PI;
pathData.push("A", radiusH, radiusV, rotation * r2d, largeArcFlag, sweepFlag, end.x, end.y);
return pathData;
}, // END function describeSVGArc
/**
* Helper function to find second-kind elliptic angles, so that the euclidean distance along the the
* elliptic sector is the same for all.
*
* Note that this is based on the full ellipse calculuation and start and end will be cropped; so the
* distance from the start angle to the first angle and/or the distance from the last angle to
* the end angle may be different to the others.
*
* Furthermore the computation is only possible on un-rotated ellipses; if your source ellipse has
* a rotation on the plane please 'rotate' the result angles afterwards to find matching angles.
*
* Returned angles are normalized to the interval `[ 0, PI*2 ]`.
*
* @param {number} radiusH - The first (horizonal) radius of the ellipse.
* @param {number} radiusV - The second (vertical) radius of the ellipse.
* @param {number} startAngle - The opening angle of your elliptic sector (please use normalized angles).
* @param {number} endAngle - The closing angle of your elliptic sector (please use normalized angles).
* @param {number} fullEllipsePointCount - The number of base segments to use from the source ellipse (12 or 16 are good numbers).
* @return {Array<number>} An array of n angles inside startAngle and endAngle (where n <= fullEllipsePointCount).
*/
equidistantVertAngles: function (radiusH, radiusV, startAngle, endAngle, fullEllipsePointCount) {
var ellipseAngles = VEllipse_1.VEllipse.utils.equidistantVertAngles(radiusH, radiusV, fullEllipsePointCount);
ellipseAngles = ellipseAngles.map(function (angle) { return VEllipseSector.ellipseSectorUtils.normalizeAngle(angle); });
var angleIsInRange = function (angle) {
if (startAngle < endAngle)
return angle >= startAngle && angle <= endAngle;
else
return angle >= startAngle || (angle <= endAngle && angle >= 0);
};
// Drop all angles outside the sector
ellipseAngles = ellipseAngles.filter(angleIsInRange);
// Now we need to sort the angles to the first one in the array is the closest to startAngle.
// --> find the angle that is closest to the start angle
var startIndex = VEllipseSector.ellipseSectorUtils.findClosestToStartAngle(startAngle, endAngle, ellipseAngles);
// Bring all angles into the correct order
// Idea: use splice or slice here?
var angles = [];
for (var i = 0; i < ellipseAngles.length; i++) {
angles.push(ellipseAngles[(startIndex + i) % ellipseAngles.length]);
}
return angles;
},
findClosestToStartAngle: function (startAngle, endAngle, ellipseAngles) {
// Note: endAngle > 0 && startAngle > 0
if (startAngle > endAngle) {
var n = ellipseAngles.length;
for (var i = 0; i < n; i++) {
var ea = geomutils_1.geomutils.wrapMinMax(ellipseAngles[i], 0, Math.PI * 2);
if (ea >= startAngle && ea >= endAngle) {
return i;
}
}
}
return 0;
},
normalizeAngle: function (angle) { return (angle < 0 ? Math.PI * 2 + angle : angle); },
/**
* Convert the elliptic arc from endpoint parameters to center parameters as described
* in the w3c svg arc implementation note.
*
* https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
*
* @param {number} x1 - The x component of the start point (end of last SVG command).
* @param {number} y1 - The y component of the start point (end of last SVG command).
* @param {number} rx - The first (horizontal) radius of the ellipse.
* @param {number} ry - The second (vertical) radius of the ellipse.
* @param {number} phi - The ellipse's rotational angle (angle of axis rotation) in radians (not in degrees as the SVG command uses!)
* @param {boolean} fa - The large-arc-flag (boolean, not 0 or 1).
* @param {boolean} fs - The sweep-flag (boolean, not 0 or 1).
* @param {number} x2 - The x component of the end point (end of last SVG command).
* @param {number} y2 - The y component of the end point (end of last SVG command).
* @returns
*/
endpointToCenterParameters: function (x1, y1, rx, ry, phi, fa, fs, x2, y2) {
// console.log("endpointToCenterParameters", x1, y1, phi, rx, ry, fa, fs, x2, y2);
// Thanks to
// https://observablehq.com/@toja/ellipse-and-elliptical-arc-conversion
var abs = Math.abs;
var sin = Math.sin;
var cos = Math.cos;
var sqrt = Math.sqrt;
var pow = function (n) {
return n * n;
};
var sinphi = sin(phi);
var cosphi = cos(phi);
// Step 1: simplify through translation/rotation
var x = (cosphi * (x1 - x2)) / 2 + (sinphi * (y1 - y2)) / 2;
var y = (-sinphi * (x1 - x2)) / 2 + (cosphi * (y1 - y2)) / 2;
var px = pow(x), py = pow(y), prx = pow(rx), pry = pow(ry);
// correct of out-of-range radii
var L = px / prx + py / pry;
if (L > 1) {
rx = sqrt(L) * abs(rx);
ry = sqrt(L) * abs(ry);
}
else {
rx = abs(rx);
ry = abs(ry);
}
// Step 2 + 3: compute center
var sign = fa === fs ? -1 : 1;
// const M: number = sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign;
var M = sqrt(Math.abs((prx * pry - prx * py - pry * px) / (prx * py + pry * px))) * sign;
var _cx = (M * (rx * y)) / ry;
var _cy = (M * (-ry * x)) / rx;
var cx = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2;
var cy = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2;
// Step 4: Compute start and end angle
var center = new Vertex_1.Vertex(cx, cy);
var axis = center.clone().addXY(rx, ry);
var ellipse = new VEllipse_1.VEllipse(center, axis, 0);
// console.log("VELLIPSE::::::", ellipse);
ellipse.rotate(phi);
var startAngle = new Line_1.Line(ellipse.center, new Vertex_1.Vertex(x1, y1)).angle();
var endAngle = new Line_1.Line(ellipse.center, new Vertex_1.Vertex(x2, y2)).angle();
return new VEllipseSector(ellipse, startAngle - phi, endAngle - phi);
}
}; // END ellipseSectorUtils
return VEllipseSector;
}());
exports.VEllipseSector = VEllipseSector;
//# sourceMappingURL=VEllipseSector.js.map