# Help with this (almost working) code to draw arbitrary rounded polygons? ie. From a series of corner points and rounding radius?

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;
}

// 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 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?
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 ) );

//close loop
triPoints.push(triPoints[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 );
}

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 ) );

//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 );

}

``````

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.

1 Like

If it is OK to have the rounding not perfectly circular, a much shorter solution is to use shapes and quadratic curves at each vertex.

In the snapshot, blue has radius=0 (i.e. no rounding), red radius = 2 (small rounding), yellow radius = 4

1 Like

I need to solve vertices that make the shape as I will use this also for 3D constructions (not just drawing lines). I am not opposed to another function like the quadratic curves you show as long as I can input the polygon corners and receive out an array of vertexes that will roughly draw it (with some parameter for the number of vertexes per rounded element).

I am sure there must be a fix for the code I shared as it already does almost everything I need minus the bug. Thanks for any further thoughts or ideas.

You can always dress the code, so that the input is a set of corner points and the output is a set of points along the rounded shape.

https://codepen.io/boytchev/full/NWmYbJm

As for any bugs in your code, it might be much easier (for someone with sufficient spare time), if there is online editable and debuggable example. Some bugs are hard to find just by looking at the code.

1 Like

That’s very interesting! Thanks for sharing. I will look at your function a bit more.

I was not familiar with that site either. I just started with ThreeJS a few days ago. It is a great system.

I put together a similar page for the existing code I had:

The trigonometry is all explained in detail here: c# - How to calculate rounded corners for a polygon? - Stack Overflow

But for some reason I can’t make it work. I made some minor changes to the functions given at that page trying to make it work, as long as the points are supplied clockwise. The square is perfect but on the triangles one point seems to get garbled no matter what I do with it.

@mjdev Maybe this topic will be useful/helpful somehow: Line with rounded corners

1 Like

Well I solved it.

Just was wrapping wrong when angle was over +/- 180 degrees.

See for code:

``````// https://discourse.threejs.org/t/help-with-this-almost-working-code-to-draw-arbitrary-rounded-polygons-ie-from-a-series-of-corner-points-and-rounding-radius/63762
// https://stackoverflow.com/questions/24771828/how-to-calculate-rounded-corners-for-a-polygon

import * as THREE from "three";
import { Vector2 } from "three";
import { Vector3 } from "three";
import { MathUtils } from 'three';

console.clear( );

var scene = new THREE.Scene();
scene.background = new THREE.Color( 'gainsboro' );

var camera = new THREE.PerspectiveCamera( 30, innerWidth/innerHeight );
camera.position.set( 0, 0, 50 );
camera.lookAt( scene.position );

var renderer = new THREE.WebGLRenderer( {antialias: true} );
renderer.setSize( innerWidth, innerHeight );
renderer.setAnimationLoop( animationLoop );
document.body.appendChild( renderer.domElement );

camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix( );
renderer.setSize( innerWidth, innerHeight );
});

function animationLoop( t ) {
renderer.render( scene, camera );
}

// next comment

new THREE.Vector2(-2,-2),
new THREE.Vector2(-2,2),
new THREE.Vector2(2,2),
new THREE.Vector2(2,-2),
]
var triangle = [
new THREE.Vector2( -10, -5 ),
new THREE.Vector2( -5, 5 ),
new THREE.Vector2(  10, -5 ),
];

function getHexagon(xDistance)		{
//https://stackoverflow.com/questions/15958391/finding-coordinates-of-hexagonal-path
//sqrt3 because sqrt (1^2 + 2^2) presumably triangle based
const sqrt3 = Math.sqrt(3);
const point0 = new Vector2(xDistance,0);
const point3 = new Vector2(-xDistance,0);
const point1 = new Vector2(xDistance * 0.5, -sqrt3 * xDistance * 0.5); //in 30 degree right triangle, sides are c=2, b=1, a=sqrt(3)
const point2 = new Vector2(-point1.x, point1.y);
const point4 = new Vector2(-point1.x, -point1.y);
const point5 = new Vector2(point1.x, -point1.y);
var result = [point0, point1, point2, point3, point4, point5];
return result;
}

}

}
const triPoints = getRoundPoints(triangle, .4, 5);
drawCyan(triPoints);
}
const triPoints = triangle.map((x)=>x);
triPoints.push(triPoints[0]); //to close loop
drawBlue(triPoints);
}

const hexPoints = getHexagon(5);
hexPoints.push(hexPoints[0]); //to close loop
drawBlue(hexPoints);
}
const hexPoints = getHexagon(5);
const hexRound = getRoundPoints(hexPoints, 2,5);
drawCyan(hexRound);
}
function drawBlue(points){

const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
const line = new THREE.Line( geometry, material );
}
function drawCyan(points){

const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color: 0x00ffff } );
const line = new THREE.Line( geometry, material );
}

//FUNCTIONS

//=============================
//MAIN FUNCTION TO ACCESS
//=============================
/** @return { Vector2[] } */
function 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 - 1; }
if (p2Index > v2PointArray.length - 1) { p2Index = 0 };

let cornerPoints = getOneRoundedCorner(v2PointArray[angPIndex], v2PointArray[p1Index], v2PointArray[p2Index], radius, pointsCount);

console.log("#",i, " NUM POINTS ", v2PointArray.length);

//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[] } */
function 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 = getLength(dx1, dy1);
var length2 = getLength(dx2, dy2);

const length = Math.min(length1, length2);

if (segment > length) {
segment = length;
}

// 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 = getProportionPoint(angularPoint, segment, length1, dx1, dy1);
const p2Cross = getProportionPoint(angularPoint, segment, length2, dx2, dy2);

console.log("p1Cross", p1Cross, " ",  segment," ", length," ", dx1," ", dy1);
console.log("p2Cross", p2Cross, " ",  segment," ", length," ", 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 = getLength(dx, dy);

const circlePoint = 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);

//for clockwise points, end angle should always
if (endAngle > startAngle){

}
//Sweep angle
var sweepAngle = endAngle - startAngle;

//============================================
//PROBLEMS FROM HERE I THINK WITH SWEEP ANGLE
//============================================
//Some additional checks //IS THIS WHERE THE PROBLEM IS?
console.log(" SWEEP ANGLE ", sweepAngDeg, " ang point ", angularPoint, " p1 ", p1, " p2 ", p2, " startAng ", startAngDeg, " endAng ", endAngDeg);

if (sweepAngle < 0) {
//startAngle = endAngle;
//sweepAngle = -sweepAngle;
}
if (sweepAngle < -Math.PI) {
sweepAngle = 2 * Math.PI + sweepAngle; //this fixes the quad but messes up the triangle

}

//wrap if >180 degrees
if (sweepAngle > Math.PI) {
sweepAngle = 2 * 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 ", sweepAngDeg);

//===========================================
//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;
}

function getLength(dx, dy) {
return Math.sqrt(dx * dx + dy * dy);
}

function getProportionPoint(point, segment, length, dx, dy) {
const factor = segment / length;
return new Vector2((point.x - dx * factor), (point.y - dy * factor));
}

``````

Thanks for the other ideas as well.

4 Likes

there is a lib dedicated to round corners of an arbitrary polygon

it can give you segments of rounded corners, from which it’s easy enough to create a geometry using BufferGeometry class