ThreeJS: How to rotate subcomponent of Object3D

Cross posting from Stack Overflow: three.js - ThreeJS: Rotate subcomponent of Object3D - Stack Overflow

In Three JS, I’ve adapted a custom THREE.Object3D from javascript - ThreeJS: applying edge geometry to ArrowHelper - Stack Overflow.

The object is comprised of a line and a ring. I want the ring to be embedded in the plane tangent to the line.

However, the code currently does not do that. Instead, the ring is “parallel” to the line in that the line passes through the ring. My attempts to change the ring’s direction have been unsuccessful.

How do I alter the ring such that it lies in the plane tangent to the line?

class KoenderinkCircle extends THREE.Object3D {

    constructor( dir, origin, length, color, edgeColor, outerRadius, innerRadius ) {

        super();
        this.type = 'KoenderinkCircle';

        if ( dir === undefined ) dir = new THREE.Vector3( 0, 0, 1 );
        if ( origin === undefined ) origin = new THREE.Vector3( 0, 0, 0 );
        if ( length === undefined ) length = 1;
        if ( color === undefined ) color = 0xffff00;
        // if ( outerRadius === undefined ) outerRadius = length;
        // if ( innerRadius === undefined ) innerRadius = 0.5 * outerRadius;
        // outerRadius = 0.5 * length;
        outerRadius = length;
        // innerRadius = 0.9 * outerRadius;
        innerRadius = 0.3 * outerRadius;

        this._lineGeometry = new THREE.BufferGeometry();
        this._lineGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
        // The last argument is the number of segments.
        this._ringGeometry = new THREE.RingGeometry(innerRadius, outerRadius, 37);
        // this._ringGeometry.translate( 0, - 0.5, 0 );
        this._axis = new THREE.Vector3();

        this.line = new THREE.Line( this._lineGeometry, new THREE.LineBasicMaterial( { color: color, toneMapped: false, linewidth: 4 } ) );
        this.add( this.line )

        this.ring = new THREE.Mesh( this._ringGeometry, new THREE.MeshBasicMaterial( { color: color, toneMapped: false } ) );
        // TODO Rotate the ring such that line is normal to plane the ring lies on.

        this.position.copy( origin );
        this.setLength( length, outerRadius, innerRadius );
        this.setDirection( dir );

    }

    setDirection( dir ) {

        // dir is assumed to be normalized

        if ( dir.y > 0.99999 ) {

            this.quaternion.set( 0, 0, 0, 1 );

        } else if ( dir.y < - 0.99999 ) {

            this.quaternion.set( 1, 0, 0, 0 );

        } else {

            this._axis.set( dir.z, 0, - dir.x ).normalize();

            const radians = Math.acos( dir.y );

            this.quaternion.setFromAxisAngle( this._axis, radians );

        }

    }

    setLength( length, headLength, headWidth ) {

        if ( headLength === undefined ) headLength = 0.2 * length;
        if ( headWidth === undefined ) headWidth = 0.2 * headLength;

        this.line.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458
        this.line.updateMatrix();

        this.ring.scale.set( headWidth, headLength, headWidth );
        // This gives the ring an offset from the base position
        // this.ring.position.y = length;

        this.ring.updateMatrix();

    }

    setColor( color ) {
        this.line.material.color.set( color );
        this.ring.material.color.set( color );
    }

    copy( source ) {
        super.copy( source, false );
        this.line.copy( source.line );
        this.ring.copy( source.ring );
        return this;
    }
}

Edit: Using an answer from How to derive "standard" rotations from three.js when using quaternions? - Stack Overflow, I used the following and it almost works. But the line still doesn’t look quite perpendicular to the ring:

        // For some reason, the only way to get a direction is by applying a quaternion to (0, 0, 1)
        const ringDirection = new THREE.Vector3( 0, 0, 1 );
        ringDirection.applyQuaternion( this.quaternion );
        const perpDirection = new THREE.Vector3(1., 1., -(ringDirection.x + ringDirection.y) / ringDirection.z)  //.normalize()
        this.ring.lookAt(perpDirection);

2 example objects (one in pink, one in teal):

enter image description here