I am trying to draw arbitrary rounded polygons (eg. triangle, quad, pentagon, hexagon) by just defining the corner points and the rounding radius.
I found this StackOverflow which laid out a method but despite hours of work, I can only get it partly working.
I think this might be a utility others might find helpful as well. I don’t really understand the trigonometry very well. So I wonder if anyone here might understand it and be able to suggest the fix.
The class code I developed is:
import { Vector2 } from "three";
import { Vector3 } from "three";
import { MathUtils } from 'three';
class RoundPolygon {
//=============================
//MAIN FUNCTION TO ACCESS
//=============================
/** @return { Vector2[] } */
static getRoundPoints(v2PointArray, radius, pointsCount) { //pointsCount is num points per each round corner
if (v2PointArray.length < 3) { return null; }
if (pointsCount <2){ pointsCount = 2; }
const totalPoints = v2PointArray.length * pointsCount;
//https://www.w3schools.com/js/js_arrays.asp //can add just by accessing "length" index, or push
var allPoints = [];
for (let i = 0; i < v2PointArray.length; i++) {
let p1Index = i - 1;
let angPIndex = i;
let p2Index = i + 1;
if (p1Index < 0) { p1Index += v2PointArray.length; }
if (p2Index > v2PointArray.length - 1) { p2Index = 0 };
let cornerPoints = this.getOneRoundedCorner(v2PointArray[angPIndex], v2PointArray[p1Index], v2PointArray[p2Index], radius, pointsCount);
//copy to return array
var startIndex = i * pointsCount;
for (let j = 0; j < pointsCount; j++) {
allPoints[startIndex + j] = cornerPoints[j];
}
}
//close path
allPoints[allPoints.length] = allPoints[0];
return allPoints;
}
/** @return { Vector2[] } */
static getOneRoundedCorner(angularPoint, p1, p2, radius, pointsCount) { //Vector2 points, here pointscount = # divisions of round corner
//Vector 1
const dx1 = angularPoint.x - p1.x;
const dy1 = angularPoint.y - p1.y;
//Vector 2
const dx2 = angularPoint.x - p2.x;
const dy2 = angularPoint.y - p2.y;
//Angle between vector 1 and vector 2 divided by 2
const angle = (Math.atan2(dy1, dx1) - Math.atan2(dy2, dx2)) / 2;
// The length of segment between angular point and the points of intersection with the circle of a given radius
const tan = Math.abs(Math.tan(angle));
var segment = radius / tan;
//Check the segment
var length1 = this.getLength(dx1, dy1);
var length2 = this.getLength(dx2, dy2);
const length = Math.min(length1, length2);
if (segment > length) {
segment = length;
radius = length * tan;
}
// Points of intersection are calculated by the proportion between the coordinates of the vector, length of vector and the length of the segment
const p1Cross = this.getProportionPoint(angularPoint, segment, length1, dx1, dy1);
const p2Cross = this.getProportionPoint(angularPoint, segment, length2, dx2, dy2);
// Calculation of the coordinates of the circle center by the addition of angular vectors
const dx = angularPoint.x * 2 - p1Cross.x - p2Cross.x;
const dy = angularPoint.y * 2 - p1Cross.y - p2Cross.y;
const L = this.getLength(dx, dy);
const d = this.getLength(segment, radius);
const circlePoint = this.getProportionPoint(angularPoint, d, L, dx, dy);
//StartAngle and EndAngle of arc
var startAngle = Math.atan2(p1Cross.y - circlePoint.y, p1Cross.x - circlePoint.x);
var endAngle = Math.atan2(p2Cross.y - circlePoint.y, p2Cross.x - circlePoint.x);
//Sweep angle
var sweepAngle = endAngle - startAngle;
//============================================
//PROBLEMS FROM HERE I THINK WITH SWEEP ANGLE
//============================================
//Some additional checks //IS THIS WHERE THE PROBLEM IS?
let startAngDeg = MathUtils.radToDeg(startAngle);
let endAngDeg = MathUtils.radToDeg(endAngle);
console.log("SWEEP ANGLE ", sweepAngle, " ang point ", angularPoint, " p1 ", p1, " p2 ", p2, " startAng ", startAngDeg, " endAng ", endAngDeg);
if (sweepAngle < 0) {
//startAngle = endAngle;
//sweepAngle = -sweepAngle;
}
if (sweepAngle < -Math.PI) {
sweepAngle = Math.PI + sweepAngle; //this fixes the quad but messes up the triangle
}
//wrap if >180 degrees
if (sweepAngle > Math.PI) {
sweepAngle = Math.PI - sweepAngle; //this fixes the quad but messes up the triangle
//sweepAngle = sweepAngle - Math.PI; //this fixes the quad but messes up the triangle
}
console.log("SWEEP ANGLE CORRECTED ", sweepAngle);
//===========================================
//solve and return incremental point array
//===========================================
const degreesPerIncrement = sweepAngle / (pointsCount - 1); //ie. if only 2 points, must go whole way in one increment
var sign = Math.sign(sweepAngle);
var points = [];
for (let i = 0; i < pointsCount; i++) { //i think this is not going to full range of arc
let pointX = circlePoint.x + Math.cos(startAngle - sign * i * degreesPerIncrement ) * radius;
let pointY = circlePoint.y + Math.sin(startAngle - sign * i * degreesPerIncrement ) * radius;
points[i] = new Vector2(pointX, pointY);
}
return points;
}
static getLength(dx, dy) {
return Math.sqrt(dx * dx + dy * dy);
}
static getProportionPoint(point, segment, length, dx, dy) {
console.log("get proportion point", point, " ", segment," ", length," ", dx," ", dy);
const factor = segment / length;
return new Vector2((point.x - dx * factor), (point.y - dy * factor));
}
static convertV2ToV3(points){ //currently along xy, can have switch var for axis
var newPoints = [];
for (let i = 0; i< points.length; i++){
newPoints[i] = new Vector3(points[i].x, points[i].y, 0);
}
return newPoints;
}
}
export { RoundPolygon };
I utilized this with the following two functions to see the unrounded and rounded element by comparison:
function drawLine(){
const triPoints = [];
triPoints.push( new THREE.Vector3( - 3, 0, 0 ) );
triPoints.push( new THREE.Vector3( 0, 3, 0 ) );
triPoints.push( new THREE.Vector3( 3, 0, 0 ) );
const quadPoints = [];
quadPoints.push(new Vector3(-1,-1,0));
quadPoints.push(new Vector3(-1,1,0));
quadPoints.push(new Vector3(1,1,0));
quadPoints.push(new Vector3(1,-1,0));
//close loop
triPoints.push(triPoints[0]);
quadPoints.push(quadPoints[0]);
//SWITCH TO TRIPOINTS IF WANT TO SEE TRIANGLE:
const geometry = new THREE.BufferGeometry().setFromPoints( quadPoints );
const material = new THREE.LineBasicMaterial( { color: 0x00ffff } );
const line = new THREE.Line( geometry, material );
scene.add(line);
}
function drawRoundPolygon(){
const triPoints = [];
triPoints.push( new THREE.Vector3( - 3, 0, 0 ) );
triPoints.push( new THREE.Vector3( 0, 3, 0 ) );
triPoints.push( new THREE.Vector3( 3, 0, 0 ) );
const quadPoints = [];
quadPoints.push(new Vector3(-1,-1,0));
quadPoints.push(new Vector3(-1,1,0));
quadPoints.push(new Vector3(1,1,0));
quadPoints.push(new Vector3(1,-1,0));
//closes loop inside function, no need to do manually
// SWITCH TO TRI POINTS HERE IF WANT TRIANGLE
const roundPoints = RoundPolygon.getRoundPoints( quadPoints, 0.45, 5 );
const geometry = new THREE.BufferGeometry().setFromPoints( roundPoints );
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
const line = new THREE.Line( geometry, material );
scene.add(line);
}
This currently gives the following perfectly for the square (quad):
But this is the best I can get on the triangle:
Does anyone who understands trigonometry better than me have a solution for this? It would be great to get it working.
Thanks for any help.