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

threejs - round 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

image

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. :slight_smile:

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

// general setup, boring, skip to the next comment

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 );
			
window.addEventListener( "resize", (event) => {
    camera.aspect = innerWidth/innerHeight;
    camera.updateProjectionMatrix( );
    renderer.setSize( innerWidth, innerHeight );
});

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

// next comment



var quad = [
  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;
}

function addQuadRound(){
    const quadPoints = getRoundPoints(quad, .5, 5);
    drawCyan(quadPoints);
}

function addQuad(){
  const quadPoints = quad.map((x)=>x);
  quadPoints.push(quadPoints[0]); //to close loop
  drawBlue(quadPoints);
}
function addTriRound(){
  const triPoints = getRoundPoints(triangle, .4, 5);
  drawCyan(triPoints);
}
function addTri(){
  	const triPoints = triangle.map((x)=>x);
    triPoints.push(triPoints[0]); //to close loop
   drawBlue(triPoints);
}

function addHexagon(){
    const hexPoints = getHexagon(5);
  hexPoints.push(hexPoints[0]); //to close loop
 drawBlue(hexPoints);
}
function addRoundHexagon(){
    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 );
  scene.add(line);
}
function drawCyan(points){
  
	const geometry = new THREE.BufferGeometry().setFromPoints( points );
	const material = new THREE.LineBasicMaterial( { color: 0x00ffff } );
	const line = new THREE.Line( geometry, material );
  scene.add(line);
}

//ADD WHATEVER TO SCENE
addTri();
addTriRound();
addQuad();
addQuadRound();
addHexagon();
addRoundHexagon();
//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;
            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 = 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 d = getLength(segment, radius);

        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?
        let sweepAngDeg = MathUtils.radToDeg(sweepAngle);
        let startAngDeg = MathUtils.radToDeg(startAngle);
        let endAngDeg = MathUtils.radToDeg(endAngle);
        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
        }
        sweepAngDeg = MathUtils.radToDeg(sweepAngle);
        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