My question is similar to How to Make a Point Orbit a Line, 3D but the answer there didn\'t seem to solve my problem. And what I am looking for is a general solution.
Did an implementation of this using the toxilib library for 3D graphics following tactic in the answer from Reto. Did two versions, getPointOnCircle uses the unforked original toxilib, and getPointOnCircleD which uses a forked toxiLib version employing doubles.
I agree with the comments that question as asked, is incomplete. The starting location of the circle is not specified (what location corresponds to angle==0.0 ?). I would add that the orientation of the circle is also not specified ( Clockwize or CounterClockwise?). To conform to the North American math and physics practice, I multiplied Reto's angles by -1 to get my desired CCW orientation for axies having left hand palm toward +X, fingers toward +Y, thumb toward +Z. In the test cases I document the orientations which result from the from the code.
My use case is to draw cylinders and helices in the https://processing.org/ environment. Toward this end, I included a method which returns both the point on the circle, and a normal thereto.
/* 9 test cases seem necessary.
* 1 from: zero length normal input (Choose to not implement protection from this condition in default method)
* 1 from: normal unaligned with any axis,
* 3 from: normal in any of the three axial planes,
* 3 from: normal along any of the three axies
* 1 from: 1.0 != normal.magnitude() (Choose to not implement protection from this condition in default method)
*/
//VecD3D normal = new VecD3D();
//normal = new VecD3D(1.0,1.0,1.0).normalize(); /* path 0, sets 0==angle near 0.7071,0.0000,0.7071 CCW from -1,-1,-1 */
//normal = new VecD3D(1.0,0.0,0.0); /* path 0, sets 0==angle at -Z CCW from -X */
//normal = new VecD3D(0.0,1.0,0.0); /* path 1, sets 0==angle at +X CCW from -Y */
//normal = new VecD3D(0.0,0.0,1.0); /* path 0, sets 0==angle at +X CCW from -Z */
//normal = new VecD3D(1.0,1.0,0.0).normalize(); /* path 0, sets 0==angle at -Z CCW from -1,-1, 0 */
//normal = new VecD3D(0.0,1.0,1.0).normalize(); /* path 0, sets 0==angle at +X CCW from 0,-1,-1 */
//normal = new VecD3D(1.0,0.0,1.0).normalize(); /* path 0, sets 0==angle at +X CCW from 1, 0, 1 */
//normal = new VecD3D(100.,100.,100.); /* path 0, sets 0==angle near 0.7071,0.0000,0.7071 CCW from -1,-1,-1 */
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the extension of the toxiclibs.org 3D vector class extension fork providing doubles based vectors https://github.com/TPMoyer/toxiclibs
* This method does not check that the normal is normalized, and does not check that the normal is not the zero vector
*/
import toxi.geom.*;
VecD3D getPointOnCircleD(VecD3D v0, VecD3D normal,double angle,double radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
if(normal.x != 0. || normal.z != 0.){
VecD3D v1 = new VecD3D(normal.z,0.0,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
//log.info("getPointOnCircleD path 0");
return (v0.add(v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle))).scale(radius)));
} else {
VecD3D v1 = new VecD3D(normal.y,0.,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
//log.info("getPointOnCircleD path 1");
return (v0.add(v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle))).scale(radius)));
}
}
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the extension of the toxiclibs.org 3D vector class extension fork into using doubles https://github.com/TPMoyer/toxiclibs
*/
VecD3D[] getPointAndNormalOnCircleD(VecD3D v0, VecD3D normal,double angle,double radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
VecD3D[] out = new VecD3D[2];
if(normal.x != 0. || normal.z != 0.){
VecD3D v1 = new VecD3D(normal.z,0.0,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
out[1]=v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle)));
out[0]=v0.add(out[1].scale(radius));
} else {
VecD3D v1 = new VecD3D(normal.y,0.,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
out[1]=v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle)));
out[0]=v0.add(out[1].scale(radius));
}
return out;
}
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the the toxiclibs.org 3D vector class http://toxiclibs.org/
*/
Vec3D getPointOnCircle(Vec3D v0, Vec3D normal,float angle,float radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
if(normal.x != 0. || normal.z != 0.){
Vec3D v1 = new Vec3D(normal.z,0.0,-normal.x).normalize();
Vec3D v2 = normal.cross(v1);
return new Vec3D((v0.add(v1.scale((float)Math.cos(-angle)).add(v2.scale((float)Math.sin(-angle))).scale(radius))));
} else {
Vec3D v1 = new Vec3D(normal.y,0.,-normal.x).normalize();
Vec3D v2 = normal.cross(v1);
return (v0.add(v1.scale((float)Math.cos(-angle)).add(v2.scale((float)Math.sin(-angle))).scale(radius)));
}
}