Line with rounded corners

The idea comes from one question on SO.
Based on another solution.

RoundedCornerLine

https://jsfiddle.net/prisoner849/eL82teuz/

This function takes 4 parameters.

  1. points: an array of THREE.Vector3()
  2. radius: a radius of rounding (default value is 0.1)
  3. smoothness: amount of segments (default value is 3)
  4. closed: boolean; indicates if the line is closed (default value is false)

Criticism is very welcomed.

  function roundedCornerLine(points, radius, smoothness, closed) {

    radius = radius !== undefined ? radius : .1;
    smoothness = smoothness !== undefined ? Math.floor(smoothness) : 3;
    closed = closed !== undefined ? closed : false;

    let newGeometry = new THREE.BufferGeometry();

    if (points === undefined) {
      console.log("RoundedCornerLine: 'points' is undefined");
      return newGeometry
    }
    if (points.length < 3) {
      console.log("RoundedCornerLine: 'points' has insufficient length (should be equal or greater than 3)");
      return newGeometry.setFromPoints(points);
    }

    // minimal segment
    let minVector = new THREE.Vector3();
    let minLength = minVector.subVectors(points[0], points[1]).length();
    for (let i = 1; i < points.length - 1; i++) {
      minLength = Math.min(minLength, minVector.subVectors(points[i], points[i + 1]).length());
    }
    if (closed) {
      minLength = Math.min(minLength, minVector.subVectors(points[points.length - 1], points[0]).length());
    }

    radius = radius > minLength * .5 ? minLength * .5 : radius; // radius can't be greater than a half of a minimal segment

    let startIndex = 1;
    let endIndex = points.length - 2;
    if (closed) {
      startIndex = 0;
      endIndex = points.length - 1;
    }

    let positions = [];
    if (!closed) {
      positions.push(points[0].clone())
    };

    for (let i = startIndex; i <= endIndex; i++) {

      let iStart = i - 1 < 0 ? points.length - 1 : i - 1;
      let iMid = i;
      let iEnd = i + 1 > points.length - 1 ? 0 : i + 1;
      let pStart = points[iStart];
      let pMid = points[iMid];
      let pEnd = points[iEnd];

      // key points
      let vStartMid = new THREE.Vector3().subVectors(pStart, pMid).normalize();
      let vEndMid = new THREE.Vector3().subVectors(pEnd, pMid).normalize();
      let vCenter = new THREE.Vector3().subVectors(vEndMid, vStartMid).divideScalar(2).add(vStartMid).normalize();
      let angle = vStartMid.angleTo(vEndMid);
      let halfAngle = angle * .5;

      let sideLength = radius / Math.tan(halfAngle);
      let centerLength = Math.sqrt(sideLength * sideLength + radius * radius);

      let startKeyPoint = vStartMid.multiplyScalar(sideLength);
      let centerKeyPoint = vCenter.multiplyScalar(centerLength);
      let endKeyPoint = vEndMid.multiplyScalar(sideLength);

      let cb = new THREE.Vector3(),
        ab = new THREE.Vector3(),
        normal = new THREE.Vector3();
      cb.subVectors(centerKeyPoint, endKeyPoint);
      ab.subVectors(startKeyPoint, endKeyPoint);
      cb.cross(ab);
      normal.copy(cb).normalize();

      let rotatingPointStart = new THREE.Vector3().subVectors(startKeyPoint, centerKeyPoint);
      let rotatingPointEnd = new THREE.Vector3().subVectors(endKeyPoint, centerKeyPoint);
      let rotatingAngle = rotatingPointStart.angleTo(rotatingPointEnd);
      let angleDelta = rotatingAngle / smoothness;
      let tempPoint = new THREE.Vector3();
      for (let a = 0; a < smoothness + 1; a++) {
        tempPoint.copy(rotatingPointStart).applyAxisAngle(normal, angleDelta * a).add(pMid).add(centerKeyPoint);
        positions.push(tempPoint.clone());
      }

    }

    if (!closed) {
      positions.push(points[points.length - 1].clone());
    } else {
      positions.push(positions[0].clone());
    }

    return newGeometry.setFromPoints(positions);

  }
2 Likes

Interesting approach. But if it’s just a 2D path, it’s maybe easier to use the built-in Path API of three.js.

Simple example: https://jsfiddle.net/f2Lommf5/702/

As you can see in the code, the round corner is created with a QuadraticBezierCurve. Users can easily adjust the shape of the corner via the respective control point (first two parameters of Path.quadraticCurveTo().

2 Likes

In my case, there’s an array of THREE.Vector3(), so you can build a line in 3D. But maybe I didn’t get something from your reply. :thinking:

You can only use the Path API for creating 2D structures which are normally used to create 2D Shapes (see ShapeGeometry).

But there are 3D variants of curves like LineCurve3 or QuadraticBezierCurve3 you could use for creating 3D curves. So create an instance of CurvePath, add your curve objects and then sample points via getPoints( subdivisions ).

1 Like

Ah, yes! Thanks for pointing to 3D variants of curves :slight_smile: :

RoundedCornerLineCurve

https://jsfiddle.net/prisoner849/v59g7jac/

I’ve compared two approaches.
The same array of points. The red line is based on my old approach. The yellow line (you can see its parts) is based on the approach with THREE.QuadraticBezierCurve3(). As you can see, the sharper an angle, the greater the difference. Using curves, I can’t control the radius. And based on this thread, I can’t do it at all.
But possibly I’m missing something. I’ll appreciate any help :slight_smile:

1 Like

The second argument of QuadraticBezierCurve3 is the control point that influences the form of the curve. If you displace this point in certain directions, you get different shapes.

Unfortunately, i can’t tell you ad hoc how to change your code so you can adjust the menioned displacement based on a parameter. I guess you have to try this out :wink:

Obviously :slight_smile:
Thanks anyway :beers:
I’ve already thought about it, while going to lunch )) But it would be great, if we were able to put weight for the second point, thus we could get points from that curve like it’s a rational one.

@prisoner849 once upon a time I built a curve visualization tool for GSAP.
If you change the curve type to Cubic or Quadratic and play with the floating control points it may help you to visualize how they work.

1 Like

Interesting tool :slight_smile:
It was enough to read about THREE.QuadraticBezierCurve3() and some articles on the internet to realize what the second point does and how it works in general.
My goal is to build a line with rounded angles of controllable radius and smoothness, using abilities of our fellow framework :slight_smile:
The approach with quadratic bezier curves promised to be simplier than mine, but inability to set and use weights for control points of curves ruins that good start :slight_smile:

QadraticBezierCurveWeight

3 Likes

@prisoner849 Out of curiosity: How would an implementation from your point of view look like that supports weights for control points? I guess we have to change the actual curve class and the respective interpolation function…

QuadraticBezierCurve3
Interpolations

The thing is: This feature would be backwards compatible since the default weight would be just one.

Yes, I’ve already looked at the source code of both curve and interpolation before I’ve made my previous post, with a small hope that I can override some methods in prototypes :slight_smile: Found it complicated, but I can’t say that it’s impossible :slight_smile: I’ll post it here, when I come up with something.

Totally agree. It should be an optional parameter with the default value of 1.

I’m that lazy ass, who doesn’t want to work much :smile:
Somehow I remembered, that once upon a time I went through the source code of examples and somewhere there I met a mention about weight for points. Quick search gave me what I was looking for. NURBS
So, I’ve just adapted that piece of code.

NURBSweight

https://jsfiddle.net/prisoner849/bnz96otu/

<script src="https://threejs.org/examples/js/curves/NURBSCurve.js"></script>
<script src="https://threejs.org/examples/js/curves/NURBSUtils.js"></script>
<script>

  function getCurveGeometry(points, smoothness, weight) {
    var nurbsControlPoints = [];
    var nurbsKnots = [];
    var nurbsDegree = 2; // quadratic
    for (let i = 0; i <= nurbsDegree; i++) {
      nurbsKnots.push(0);
    }
    for (let i = 0, j = points.length; i < j; i++) {
      nurbsControlPoints.push(
        new THREE.Vector4(
          points[i].x,
          points[i].y,
          points[i].z,
          i === 1 ? weight : 1
        )
      );
      let knot = (i + 1) / (j - nurbsDegree);
      nurbsKnots.push(THREE.Math.clamp(knot, 0, 1));
    }
    let nurbsCurve = new THREE.NURBSCurve(nurbsDegree, nurbsKnots, nurbsControlPoints);
    return new THREE.BufferGeometry().setFromPoints(nurbsCurve.getPoints(smoothness));
  }

</script>

The other question, does this approach make the solution simplier than my original one? :thinking:

1 Like

How do you know what weight to apply to make it perfectly circular? Seems like just specifying a radius is easier for an end user.

Have you by chance a rounded corner rectangle implementation?

@trusktr
That’s how you know the weight. :slight_smile:

Specifically for the example with NURBS, you just set point’s weight manually by GUI:)

Well, you can make it with four quarters of a circle shape, using shape.absarc().
Like I did it here:

shape.absarc( eps, eps, eps, -Math.PI / 2, -Math.PI, true ); 
shape.absarc( eps, height - radius * 2, eps, Math.PI, Math.PI / 2, true ); 
shape.absarc( width - radius * 2, height - radius * 2, eps, Math.PI / 2, 0, true ); 
shape.absarc( width - radius * 2, eps, eps, 0, -Math.PI / 2, true );
2 Likes