/*
Copyright 2008-2026
Matthias Ehmann,
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 Geometry from "../math/geometry.js";
import Type from "../utils/type.js";
import Mat from "../math/math.js";
/**
* Constructor for 3D curves.
* @class Creates a new 3D curve object. Do not use this constructor to create a 3D curve. Use {@link JXG.View3D#create} with type {@link Curve3D} instead.
*
* @augments JXG.GeometryElement3D
* @augments JXG.GeometryElement
* @param {View3D} view
* @param {Function} F
* @param {Function} X
* @param {Function} Y
* @param {Function} Z
* @param {Array} range
* @param {Object} attributes
* @see JXG.Board#generateName
*/
JXG.Curve3D = function (view, F, X, Y, Z, range, attributes) {
this.constructor(view.board, attributes, Const.OBJECT_TYPE_CURVE3D, Const.OBJECT_CLASS_3D);
this.constructor3D(view, 'curve3d');
this.board.finalizeAdding(this);
/**
* Internal function defining the surface without applying any transformations.
* Does only exist if it or X are supplied as a function. Otherwise it is null.
*
* @function
* @private
*/
this._F = F;
/**
* Function or array which maps u to x; i.e. it defines the x-coordinate of the curve
* @function
* @returns Number
* @private
*/
this._X = X;
/**
* Function or array which maps u to y; i.e. it defines the y-coordinate of the curve
* @function
* @returns Number
* @private
*/
this._Y = Y;
/**
* Function or array which maps u to z; i.e. it defines the z-coordinate of the curve
* @function
* @returns Number
* @private
*/
this._Z = Z;
this.points = [];
this.numberPoints = 0;
this.dataX = null;
this.dataY = null;
this.dataZ = null;
if (this._F !== null) {
this._X = function (u) {
return this._F(u)[0];
};
this._Y = function (u) {
return this._F(u)[1];
};
this._Z = function (u) {
return this._F(u)[2];
};
} else {
if (Type.isFunction(this._X)) {
this._F = function(u) {
return [this._X(u), this._Y(u), this._Z(u)];
};
} else {
this._F = null;
}
}
this.range = range;
};
JXG.Curve3D.prototype = new JXG.GeometryElement();
Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, 'constructor3D');
Type.copyMethodMap(JXG.Curve3D, {
// TODO
});
JXG.extend(
JXG.Curve3D.prototype,
/** @lends JXG.Curve3D.prototype */ {
/**
* Simple curve plotting algorithm.
*
* @returns {JXG.Curve3D} Reference to itself
*/
updateCoords: function() {
var steps = this.evalVisProp('numberpointshigh'),
r, s, e, delta,
u, i,
c3d = [1, 0, 0, 0];
this.points = [];
if (Type.exists(this.dataX)) {
steps = this.dataX.length;
for (u = 0; u < steps; u++) {
this.points.push([1, this.dataX[u], this.dataY[u], this.dataZ[u]]);
}
} else if (Type.isArray(this._X)) {
steps = this._X.length;
for (u = 0; u < steps; u++) {
this.points.push([1, this._X[u], this._Y[u], this._Z[u]]);
}
} else {
r = Type.evaluate(this.range);
s = Type.evaluate(r[0]);
e = Type.evaluate(r[1]);
delta = (e - s) / (steps - 1);
for (i = 0, u = s; i < steps && u <= e; i++, u += delta) {
c3d = this.F(u);
c3d.unshift(1);
this.points.push(c3d);
}
}
this.numberPoints = this.points.length;
return this;
},
/**
* Generic function which evaluates the function term of the curve
* and applies its transformations.
* @param {Number} u
* @returns {Array} Image `[x, y, z]` of function at `u`
*/
evalF: function(u) {
var t, i,
c3d = [0, 0, 0, 0];
if (this.transformations.length === 0 || !Type.exists(this.baseElement)) {
if (Type.exists(this._F)) {
c3d = this._F(u);
} else {
c3d = [this._X[u], this._Y[u], this._Z[u]];
}
return c3d;
}
t = this.transformations;
for (i = 0; i < t.length; i++) {
t[i].update();
}
if (c3d.length === 3) {
c3d.unshift(1);
}
if (this === this.baseElement) {
if (Type.exists(this._F)) {
c3d = this._F(u);
} else {
c3d = [this._X[u], this._Y[u], this._Z[u]];
}
} else {
c3d = this.baseElement.evalF(u);
}
c3d.unshift(1);
c3d = Mat.matVecMult(t[0].matrix, c3d);
for (i = 1; i < t.length; i++) {
c3d = Mat.matVecMult(t[i].matrix, c3d);
}
return c3d.slice(1);
},
/**
* Function defining the curve plus applying transformations.
* @param {Number} u
* @returns Array [x, y, z] of length 3
*/
F: function(u) {
return this.evalF(u);
},
/**
* Function which maps (u) to z; i.e. it defines the x-coordinate of the curve
* plus applying transformations.
* @param {Number} u
* @returns Number
*/
X: function(u) {
return this.evalF(u)[0];
},
/**
* Function which maps (u) to y; i.e. it defines the y-coordinate of the curve
* plus applying transformations.
* @param {Number} u
* @returns Number
*/
Y: function(u) {
return this.evalF(u)[1];
},
/**
* Function which maps (u) to z; i.e. it defines the z-coordinate of the curve
* plus applying transformations.
* @param {Number} u
* @returns Number
*/
Z: function(u) {
return this.evalF(u)[2];
},
updateDataArray2D: function () {
var i, c2d,
dataX = [],
dataY = [],
len = this.points.length;
for (i = 0; i < len; i++) {
c2d = this.view.project3DTo2D(this.points[i]);
dataX.push(c2d[1]);
dataY.push(c2d[2]);
}
return { X: dataX, Y: dataY };
},
// Already documented in GeometryElement
addTransform: function (el, transform) {
this.addTransformGeneric(el, transform);
return this;
},
// Already documented in GeometryElement
removeTransform: function (transform) {
this.removeTransformGeneric(transform);
return this;
},
// Already documented in GeometryElement
clearTransforms: function () {
this.clearTransformsGeneric();
return this;
},
/**
*
* @returns {JXG.Curve3D} Reference to itself
*/
updateTransform: function () {
var t, c, i, j, len;
if (this.transformations.length === 0 || this.baseElement === null ||
Type.exists(this._F) // Transformations have only to be applied here
// if the curve is defined by arrays
) {
return this;
}
t = this.transformations;
for (i = 0; i < t.length; i++) {
t[i].update();
}
len = this.baseElement.numberPoints;
for (i = 0; i < len; i++) {
if (this === this.baseElement) {
c = this.points[i];
} else {
c = this.baseElement.points[i];
}
for (j = 0; j < t.length; j++) {
c = Mat.matVecMult(t[j].matrix, c);
}
this.points[i] = c;
}
this.numberPoints = len;
return this;
},
// Already documented in GeometryElement
updateDataArray: function() { /* stub */ },
// Already documented in GeometryElement
update: function () {
if (this.needsUpdate) {
this.updateDataArray();
this.updateCoords()
.updateTransform();
}
return this;
},
// Already documented in GeometryElement
updateRenderer: function () {
this.needsUpdate = false;
return this;
},
// Already documented in element3d.js
projectCoords: function (p, params) {
return Geometry.projectCoordsToParametric(p, this, 1, params);
}
// Use method from element3d.js
// projectScreenCoords: function (pScr, params, cyclic) {
// this.initParamsIfNeeded(params);
// return Geometry.projectScreenCoordsToParametric(pScr, this, params, cyclic);
// }
}
);
/**
* @class 3D Curves can be defined by mappings or by discrete data sets.
* In general, a 3D curve is a mapping from R to R^3, where t maps to (x(t),y(t),z(t)).
* The graph is drawn for t in the interval [a,b].
* A 3D parametric curve is defined by a function
* \\[F: {\mathbb R} \to {\mathbb R}^3.\\]
*
* @pseudo
* @name Curve3D
* @elementclass 3D
* @constructor
* @type Object
* @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
*/
/**
* @jsxgraphsignature Curve3D
* F<sub>X</sub>(u), F<sub>Y</sub>(u), F<sub>Z</sub>(u) are functions returning a number, range is the array containing
* lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two.
* @param {Function} F<sub>X</sub>
* @param {Function} F<sub>Y</sub>
* @param {Function} F<sub>Z</sub>
* @param {Array|Function} range
*
* @example <caption>Create a simple curve in 3D</caption>
* var bound = [-1.5, 1.5];
* var view=board.create('view3d',
* [[-4, -4],[8, 8],
* [bound, bound, bound]],
* {});
* var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
* </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3',
* {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
* // create a simple curve in 3d
* var bound = [-1.5, 1.5];
* var view=board.create('view3d',
* [[-4, -4],[8, 8],
* [bound, bound, bound]],
* {});
* var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
* })();
* </script><pre>
*/
/**
* @jsxgraphsignature Curve3D
* F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of numbers, range as above.
* @param {Function} F<sub>Z</sub>
* @param {Array|Function} range
*
*/
/**
* @jsxgraphsignature Curve3D
* Three arrays containing the coordinate points which define the curve.
* @param {Array} X
* @param {Array} Y
* @param {Array} Z
*/
JXG.createCurve3D = function (board, parents, attributes) {
var view = parents[0],
F, X, Y, Z, range, attr, el,
mat,
base = null,
transform = null;
if (parents.length === 3) {
if (Type.isTransformationOrArray(parents[2]) && parents[1].type === Const.OBJECT_TYPE_CURVE3D) {
// [curve, transformation(s)]
// This might be adopted to the type of the base element (data plot or function)
base = parents[1];
transform = parents[2];
F = null;
X = [];
Y = [];
Z = [];
} else {
// [F, range]
F = parents[1];
range = parents[2];
X = null;
Y = null;
Z = null;
}
} else if (parents.length === 2 && Type.isArray(parents[1])) {
mat = Mat.transpose(parents[1]);
X = mat[0];
Y = mat[1];
Z = mat[2];
F = null;
} else {
// [X, Y, Z, range]
X = parents[1];
Y = parents[2];
Z = parents[3];
range = parents[4];
F = null;
}
// TODO Throw new Error
attr = Type.copyAttributes(attributes, board.options, 'curve3d');
el = new JXG.Curve3D(view, F, X, Y, Z, range, attr);
attr = el.setAttr2D(attr);
el.element2D = view.create("curve", [[], []], attr);
el.element2D.view = view;
el.element2D.dump = false;
if (base !== null) {
el.addTransform(base, transform);
el.addParents(base);
}
/**
* @class
* @ignore
*/
el.element2D.updateDataArray = function () {
var ret = el.updateDataArray2D();
this.dataX = ret.X;
this.dataY = ret.Y;
};
el.addChild(el.element2D);
el.inherits.push(el.element2D);
el.element2D.setParents(el);
el.element2D.prepareUpdate().update();
if (!board.isSuspendedUpdate) {
el.element2D.updateVisibility().updateRenderer();
}
return el;
};
JXG.registerElement("curve3d", JXG.createCurve3D);
/**
* @class A vector field is an assignment of a vector to each point in 3D space.
*
* Plot a vector field either given by three functions
* \\(f_1(x, y, z)\\), \\(f_2(x, y, z)\\), and \\(f_3(x, y, z)\\) or by a function \\(f(x, y, z)\\)
* returning an array of size 3.
*
* @pseudo
* @name Vectorfield3D
* @elementclass 3D
* @augments JXG.Curve3D
* @constructor
* @type JXG.Curve3D
* @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
*
*/
/**
* @jsxgraphsignature Vectorfield3D
* Either an array containing three functions `f1(x, y, z)`, `f2(x, y, z)`, and `f3(x, y, z)`
* @param {Array} f1
* @param {Array} f2
* @param {Array} f3
*
* @example
* const view = board.create('view3d',
* [
* [-6, -3],
* [8, 8],
* [[-3, 3], [-3, 3], [-3, 3]]
* ], {});
*
* var vf = view.create('vectorfield3d', [
* [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
* [-2, 5, 2], // x from -2 to 2 in 5 steps
* [-2, 5, 2], // y
* [-2, 5, 2] // z
* ], {
* strokeColor: 'red',
* scale: 0.5
* });
*
* </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348',
* {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false,
* pan: {
* needTwoFingers: true
* }
* });
* const view = board.create('view3d',
* [
* [-6, -3],
* [8, 8],
* [[-3, 3], [-3, 3], [-3, 3]]
* ], {});
* var vf = view.create('vectorfield3d', [
* [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
* [-2, 5, 2], // x from -2 to 2 in 5 steps
* [-2, 5, 2], // y
* [-2, 5, 2] // z
* ], {
* strokeColor: 'red',
* scale: 0.5
* });
*
*
* })();
*
* </script><pre>
*
*/
/**
* @jsxgraphsignature Vectorfield3D
* Function f(x, y, z) returning an array of length 3. The function may be given as JessieCode string.
* @param {Function|String} F
*/
/**
* @jsxgraphsignature Vectorfield3D
* @param {Array} xData Array of length 3 containing start value for x, number of steps,
* end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x.
* @param {Array} yData Array of length 3 containing start value for y, number of steps,
* end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y.
* @param {Array} zData Array of length 3 containing start value for z, number of steps,
* end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z.
*
*/
JXG.createVectorfield3D = function (board, parents, attributes) {
var view = parents[0],
el, attr;
if (!(parents.length >= 5 &&
(Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) &&
(Type.isArray(parents[2]) && parents[1].length === 3) &&
(Type.isArray(parents[3]) && parents[2].length === 3) &&
(Type.isArray(parents[4]) && parents[3].length === 3)
)) {
throw new Error(
"JSXGraph: Can't create vector field 3D with parent types " +
"'" + typeof parents[1] + "', " +
"'" + typeof parents[2] + "', " +
"'" + typeof parents[3] + "'." +
"'" + typeof parents[4] + "', "
);
}
attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d');
el = view.create('curve3d', [[], [], []], attr);
/**
* Set the defining functions of 3D vector field.
* @memberOf Vectorfield3D
* @name setF
* @function
* @param {Array|Function} func Either an array containing three functions f1(x, y, z),
* f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3.
* @returns {Object} Reference to the 3D vector field object.
*
* @example
* field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]);
* board.update();
*
*/
el.setF = function (func, varnames) {
var f0, f1, f2;
if (Type.isArray(func)) {
f0 = Type.createFunction(func[0], this.board, varnames);
f1 = Type.createFunction(func[1], this.board, varnames);
f2 = Type.createFunction(func[2], this.board, varnames);
/**
* @ignore
*/
this.F = function (x, y, z) {
return [f0(x, y, z), f1(x, y, z), f2(x, y, z)];
};
} else {
this.F = Type.createFunction(func, el.board, varnames);
}
return this;
};
el.setF(parents[1], 'x, y, z');
el.xData = parents[2];
el.yData = parents[3];
el.zData = parents[4];
el.updateDataArray = function () {
var k, i, j,
v, nrm,
x, y, z,
scale = this.evalVisProp('scale'),
start = [
Type.evaluate(this.xData[0]),
Type.evaluate(this.yData[0]),
Type.evaluate(this.zData[0])
],
steps = [
Type.evaluate(this.xData[1]),
Type.evaluate(this.yData[1]),
Type.evaluate(this.zData[1])
],
end = [
Type.evaluate(this.xData[2]),
Type.evaluate(this.yData[2]),
Type.evaluate(this.zData[2])
],
delta = [
(end[0] - start[0]) / steps[0],
(end[1] - start[1]) / steps[1],
(end[2] - start[2]) / steps[2]
],
phi, theta1, theta2, theta,
showArrow = this.evalVisProp('arrowhead.enabled'),
leg, leg_x, leg_y, leg_z, alpha;
if (showArrow) {
// Arrow head style
// leg = 8;
// alpha = Math.PI * 0.125;
leg = this.evalVisProp('arrowhead.size');
alpha = this.evalVisProp('arrowhead.angle');
leg_x = leg / board.unitX;
leg_y = leg / board.unitY;
leg_z = leg / Math.sqrt(board.unitX * board.unitY);
}
this.dataX = [];
this.dataY = [];
this.dataZ = [];
for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) {
for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) {
for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) {
v = this.F(x, y, z);
nrm = Mat.norm(v);
if (nrm < Number.EPSILON) {
continue;
}
v[0] *= scale;
v[1] *= scale;
v[2] *= scale;
Type.concat(this.dataX, [x, x + v[0], NaN]);
Type.concat(this.dataY, [y, y + v[1], NaN]);
Type.concat(this.dataZ, [z, z + v[2], NaN]);
if (showArrow) {
// Arrow head
nrm *= scale;
phi = Math.atan2(v[1], v[0]);
theta = Math.asin(v[2] / nrm);
theta1 = theta - alpha;
theta2 = theta + alpha;
Type.concat(this.dataX, [
x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1),
x + v[0],
x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2),
NaN]);
Type.concat(this.dataY, [
y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1),
y + v[1],
y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2),
NaN]);
Type.concat(this.dataZ, [
z + v[2] - leg_z * Math.sin(theta2),
z + v[2],
z + v[2] - leg_z * Math.sin(theta1),
NaN]);
}
}
}
}
};
Type.extendInstanceMethodMap(el, {
setF: "setF"
});
return el;
};
JXG.registerElement("vectorfield3D", JXG.createVectorfield3D);