/*
Copyright 2008-2026
Matthias Ehmann,
Aaron Fenyes,
Carsten Miller,
Andreas Walter,
Alfred Wassermann
This file is part of JSXGraph.
JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
You can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version
OR
* MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
JSXGraph is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License and
the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
and <https://opensource.org/licenses/MIT/>.
*/
/*global JXG:true, define: true*/
import JXG from "../jxg.js";
import Const from "../base/constants.js";
import Type from "../utils/type.js";
import Mat from '../math/math.js';
import Geometry from '../math/geometry.js';
/**
* In 3D space, a circle consists of all points on a given plane with a given distance from a given point. The given point is called the center, and the given distance is called the radius.
* A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function).
* @class Creates a new 3D circle object. Do not use this constructor to create a 3D circle. Use {@link JXG.View3D#create} with
* type {@link Circle3D} instead.
* @constructor
* @augments JXG.Curve3D
* @augments JXG.GeometryElement
* @param {View3D} view The 3D view the circle is drawn on.
* @param {Point3D} center The center of the circle.
* @param {Array} normal A normal vector of the plane the circle lies in. Must be either an array of three numbers or an array of three functions returning numbers.
* @param {Number|Function} radius The radius of the circle.
* @param {Object} attributes
* @see JXG.Board#generateName
*/
JXG.Circle3D = function (view, center, normal, radius, attributes) {
var altFrame1, that;
this.constructor(view.board, attributes, Const.OBJECT_TYPE_CIRCLE3D, Const.OBJECT_CLASS_3D);
this.constructor3D(view, 'circle3d');
/**
* The circle's center. Do not set this parameter directly, as that will break JSXGraph's update system.
* @type JXG.Point3D
*/
this.center = this.board.select(center);
this.normalFunc = normal;
/**
* A normal vector of the plane the circle lies in. Do not set this parameter directly, as that will break JSXGraph's update system.
* @type Array
* @private
*
* @see JXG.Circle3D#updateNormal
*/
this.normal = [0, 0, 0, 0];
/**
* The circle's underlying Curve3D.
*/
this.curve;
/**
* The first vector in an orthonormal frame for the plane the circle lies in.
* Do not set this parameter directly, as that will break JSXGraph's update system.
* @type Array
* @private
*
* @see JXG.Circle3D#updateFrame
*/
this.frame1;
/**
* The second vector in an orthonormal frame for the plane the circle lies in.
* Do not set this parameter directly, as that will break JSXGraph's update system.
* @type Array
* @private
*
* @see JXG.Circle3D#updateFrame
*/
this.frame2;
// place the circle or its center---whichever is newer---in the scene tree
if (Type.exists(this.center._is_new)) {
this.addChild(this.center);
delete this.center._is_new;
} else {
this.center.addChild(this);
}
// Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
this.updateRadius = Type.createFunction(radius, this.board);
this.addParentsFromJCFunctions([this.updateRadius]);
// initialize normal
this.updateNormal();
// initialize the first frame vector by taking the cross product with
// [1, 0, 0] or [-0.5, sqrt(3)/2, 0]---whichever is further away on the unit
// sphere. every vector is at least 60 degrees from one of these, which
// should be good enough to make the frame vector numerically accurate
this.frame1 = Mat.crossProduct(this.normal.slice(1), [1, 0, 0]);
this.frame1.unshift(0);
altFrame1 = Mat.crossProduct(this.normal.slice(1), [-0.5, 0.8660254037844386, 0]); // [1/2, sqrt(3)/2, 0]
altFrame1.unshift(0);
if (Mat.norm(altFrame1) > Mat.norm(this.frame1)) {
this.frame1 = altFrame1;
}
// initialize the second frame vector
this.frame2 = Mat.crossProduct(this.normal.slice(1), this.frame1.slice(1));
this.frame2.unshift(0);
// scale both frame vectors to unit length
this.normalizeFrame();
// create the underlying curve
that = this;
this.curve = view.create(
'curve3d',
[
function(t) {
var r = that.Radius(),
s = Math.sin(t),
c = Math.cos(t);
return [
that.center.coords[1] + r * (c * that.frame1[1] + s * that.frame2[1]),
that.center.coords[2] + r * (c * that.frame1[2] + s * that.frame2[2]),
that.center.coords[3] + r * (c * that.frame1[3] + s * that.frame2[3])
];
},
[0, 2 * Math.PI] // parameter range
],
attributes
);
};
JXG.Circle3D.prototype = new JXG.GeometryElement();
Type.copyPrototypeMethods(JXG.Circle3D, JXG.GeometryElement3D, 'constructor3D');
JXG.extend(
JXG.Circle3D.prototype,
/** @lends JXG.Circle3D.prototype */ {
// Already documented in element3d.js
update: function () {
if (this.needsUpdate) {
this.updateNormal()
.updateFrame();
this.curve.visProp.visible = !isNaN(this.Radius()); // TODO
}
return this;
},
// Already documented in element3d.js
updateRenderer: function () {
this.needsUpdate = false;
return this;
},
/**
* Set a new radius, then update the board.
* @param {String|Number|function} r A string, function or number describing the new radius
* @returns {JXG.Circle3D} Reference to this sphere
*/
setRadius: function (r) {
this.updateRadius = Type.createFunction(r, this.board);
this.addParentsFromJCFunctions([this.updateRadius]);
this.board.update();
return this;
},
/**
* Calculates the radius of the circle.
* @param {String|Number|function} [value] Set new radius
* @returns {Number} The radius of the circle
*/
Radius: function (value) {
if (Type.exists(value)) {
this.setRadius(value);
return this.Radius();
}
return Math.abs(this.updateRadius());
},
normalizeFrame: function () {
// normalize frame
var len1 = Mat.norm(this.frame1),
len2 = Mat.norm(this.frame2),
i;
for (i = 0; i < 4; i++) {
this.frame1[i] /= len1;
this.frame2[i] /= len2;
}
return this;
},
/**
* Update data for normal.
*
* @returns {JXG.Circle3D} Reference to this sphere
*/
updateNormal: function () {
// evaluate normal direction
var i, len,
eps = Mat.eps * Mat.eps;
this.normal = Type.evaluate(this.normalFunc);
// scale normal to unit length
len = Mat.norm(this.normal);
if (Math.abs(len) > eps) {
for (i = 0; i < 4; i++) {
this.normal[i] /= len;
}
}
return this;
},
/**
* Update frame.
*
* @returns {JXG.Circle3D} Reference to this sphere
*/
updateFrame: function () {
this.frame1 = Mat.crossProduct(this.frame2.slice(1), this.normal.slice(1));
this.frame1.unshift(0);
this.frame2 = Mat.crossProduct(this.normal.slice(1), this.frame1.slice(1));
this.frame2.unshift(0);
this.normalizeFrame();
return this;
},
// Already documented in element3d.js
projectCoords: function (p, params) {
// we have to call `this.curve.projectCoords`, i.e. the curve's projectCoords rather
// than the circle's, to make `this` refer to the curve within the
// call.
return this.curve.projectCoords(p, params);
},
projectScreenCoords: function (pScr, params, cyclic) {
// We have to call `this.curve.projectScreenCoords` from the curve,
// rather than the circle, to make `this` refer to the curve within
// the call
return this.curve.projectScreenCoords(pScr, params, cyclic);
}
}
);
/**
* @class A circle in 3D can be defined by various combinations of points and numbers.
* In 3D space, a circle consists of all points on a given plane with a given distance from a given point.
* The given point is called the center, and the given distance is called the radius.
* A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function).
*
* If the radius has a negative value, its absolute value is taken. If the radius evaluates to NaN,
* the circle is not displayed. This is convenient for constructing an intersection circle, which is empty when its parents do not intersect.
* @pseudo
* @name Circle3D
* @elementclass 3D
* @augments JXG.Circle3D
* @constructor
* @type JXG.Circle3D
* @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
*/
/**
* @jsxgraphsignature Circle3D
* The normal vector can be given as an array of four numbers (i.e. homogeneous coordinates `[0, x, y, z]`) or a function returning an array of length 4
* and the radius can be given as a number (which will create a circle with a fixed radius) or a function.
*
* If the radius is supplied as a number or the output of a function, its absolute value is taken.
* If the radius evaluates to NaN, the circle does not display.
*
* @param {Point3DLike} center
* @param {Array|Function} normal
* @param {NumberLike} radius
*/
JXG.createCircle3D = function (board, parents, attributes) {
var view = parents[0],
attr = Type.copyAttributes(attributes, board.options, 'circle3d'),
center = Type.providePoints3D(view, [parents[1]], attributes, 'circle3d', ['point'])[0],
normal = parents[2],
radius = parents[3],
el;
if (Type.isArray(normal) && normal.length === 3) {
normal.unshift(0);
}
// Create element
el = new JXG.Circle3D(view, center, normal, radius, attr);
// Update scene tree
el.curve.addParents([el]);
el.addChild(el.curve);
el.update();
return el;
};
JXG.registerElement("circle3d", JXG.createCircle3D);
/**
* @class The circle that is the intersection of two elements (plane3d or sphere3d) in 3D.
*
* @pseudo
* @name IntersectionCircle3D
* @elementclass 3D
* @augments JXG.Circle3D
* @constructor
* @type JXG.Circle3D
* @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
*/
/**
* @jsxgraphsignature IntersectionCircle3D
* The result will be the intersection of `el1` and `el2`.
* @param {Sphere3D} el1
* @param {Sphere3D|Plane3D} el2
*
* @example <caption>Create the intersection circle of two spheres</caption>
* var view = board.create(
* 'view3d',
* [[-6, -3], [8, 8],
* [[0, 3], [0, 3], [0, 3]]],
* {
* xPlaneRear: {fillOpacity: 0.2, gradient: null},
* yPlaneRear: {fillOpacity: 0.2, gradient: null},
* zPlaneRear: {fillOpacity: 0.2, gradient: null}
* }
* );
* var a1 = view.create('point3d', [-1, 0, 0]);
* var a2 = view.create('point3d', [1, 0, 0]);
*
* var s1 = view.create(
* 'sphere3d',
* [a1, 2],
* {fillColor: '#00ff80'}
* );
* var s2 = view.create(
* 'sphere3d',
* [a2, 2],
* {fillColor: '#ff0000'}
* );
*
* var i = view.create('intersectioncircle3d', [s1, s2]);
*
* </pre><div id="JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var view = board.create(
* 'view3d',
* [[-6, -3], [8, 8],
* [[0, 3], [0, 3], [0, 3]]],
* {
* xPlaneRear: {fillOpacity: 0.2, gradient: null},
* yPlaneRear: {fillOpacity: 0.2, gradient: null},
* zPlaneRear: {fillOpacity: 0.2, gradient: null}
* }
* );
* var a1 = view.create('point3d', [-1, 0, 0]);
* var a2 = view.create('point3d', [1, 0, 0]);
*
* var s1 = view.create(
* 'sphere3d',
* [a1, 2],
* {fillColor: '#00ff80'}
* );
* var s2 = view.create(
* 'sphere3d',
* [a2, 2],
* {fillColor: '#ff0000'}
* );
* var i = view.create('intersectioncircle3d', [s1, s2]);
*
* })();
*
* </script><pre>
*
*/
JXG.createIntersectionCircle3D = function (board, parents, attributes) {
var view = parents[0],
el1 = parents[1],
el2 = parents[2],
ixnCircle, center, func,
attr = Type.copyAttributes(attributes, board.options, 'intersectioncircle3d');
try {
func = Geometry.intersectionFunction3D(view, el1, el2);
center = view.create('point3d', func[0], { visible: false });
ixnCircle = view.create('circle3d', [center, func[1], func[2]], attr);
el1.addChild(ixnCircle);
el2.addChild(ixnCircle);
} catch (e) {
throw new Error(
"JSXGraph: Can't create 'intersection' with parent types '" +
typeof parents[1] +
"' and '" +
typeof parents[2] +
"'."
);
}
ixnCircle.type = Const.OBJECT_TYPE_INTERSECTION_CIRCLE3D;
ixnCircle.elType = 'intersectioncircle3d';
ixnCircle.setParents([el1.id, el2.id]);
return ixnCircle;
};
JXG.registerElement('intersectioncircle3d', JXG.createIntersectionCircle3D);