/*
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/>.
*/
/*
Some functionalities in this file were developed as part of a software project
with students. We would like to thank all contributors for their help:
Winter semester 2023/2024:
Lars Hofmann
Leonhard Iser
Vincent Kulicke
Laura Rinas
*/
/*global JXG:true, define: true*/
import JXG from "../jxg.js";
import Const from "../base/constants.js";
import Coords from "../base/coords.js";
import Type from "../utils/type.js";
import Mat from "../math/math.js";
import Geometry from "../math/geometry.js";
import Numerics from "../math/numerics.js";
import Env from "../utils/env.js";
import GeometryElement from "../base/element.js";
import Composition from "../base/composition.js";
/**
* @class 3D view inside a JXGraph board.
*
* Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with
* type {@link View3D} instead.
*
* @augments JXG.GeometryElement
* @param {Array} parents Array consisting of lower left corner `[x, y]` of the view inside the board, `[width, height]` of the view
* and box size `[[x1, x2], [y1,y2], [z1,z2]]`. If the view's `azimuth=0` and `elevation=0`, the 3D view will cover a rectangle with lower left corner
* `[x,y]` and side lengths `[w, h]` of the board.
*/
JXG.View3D = function (board, parents, attributes) {
this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D);
/**
* An associative array containing all geometric objects belonging to the view.
* Key is the id of the object and value is a reference to the object.
* @type Object
* @private
*/
this.objects = {};
/**
* An array containing all the elements in the view that are sorted due to their depth order.
* @type Object
* @private
*/
this.depthOrdered = {};
/**
* TODO: why deleted?
* An array containing all geometric objects in this view in the order of construction.
* @type Array
* @private
*/
// this.objectsList = [];
/**
* An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object.
* @type Object
* @private
*/
this.elementsByName = {};
/**
* Default axes of the 3D view, contains the axes of the view or null.
*
* @type {Object}
* @default null
*/
this.defaultAxes = null;
/**
* The Tait-Bryan angles specifying the view box orientation
*/
this.angles = {
az: null,
el: null,
bank: null
};
/**
* The view box orientation matrix
* @type {Array}
*/
this.matrix3DRot = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
// Used for z-index computation
this.matrix3DRotShift = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
/**
* @type {Array}
* @private
*/
// 3D-to-2D transformation matrix
this.matrix3D = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0]
];
/**
* The 4×4 matrix that maps box coordinates to camera coordinates. These
* coordinate systems fit into the View3D coordinate atlas as follows.
*
* - __World coordinates.__ The coordinates used to specify object
* positions in a JSXGraph scene.
* - __Box coordinates.__ The world coordinates translated to put the
* center of the view box at the origin.
* - __Camera coordinates.__ The coordinate system where the
* `x`, `y` plane is the screen, the origin is the
* center of the screen, and the `z` axis points out of the
* screen, toward the viewer.
* - __Focal coordinates.__ The camera coordinates translated to put
* the origin at the focal point, which is set back from the screen by the
* focal distance.
*
* The `boxToCam` transformation is exposed to help 3D elements
* manage their 2D representations in central projection mode. To map world
* coordinates to focal coordinates, use the
* {@link JXG.View3D#worldToFocal} method.
* @type {Array}
*/
this.boxToCam = [];
/**
* @type array
* @private
*/
// Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0.
this.llftCorner = parents[0];
/**
* Width and height [w, h] of the 3D view if elevation and azimuth are set to 0.
* @type array
* @private
*/
this.size = parents[1];
/**
* Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view
* @type array
*/
this.bbox3D = parents[2];
/**
* The distance from the camera to the origin. In other words, the
* radius of the sphere where the camera sits.
* @type Number
* @default null
*/
this.r = null;
/**
* The distance from the camera to the screen. Computed automatically from
* the `fov` property.
* @type Number
*/
this.focalDist = -1;
/**
* Type of projection. Is set in in update().
* @type String
*/
this.projectionType = 'parallel';
/**
* Whether trackball navigation is currently enabled.
* @type String
*/
this.trackballEnabled = false;
/**
* Store last position of pointer.
* This is the successor to use evt.movementX/Y which caused problems on firefox
* @type Object
* @private
*/
this._lastPos = {
x: 0,
y: 0
};
this.timeoutAzimuth = null;
this.zIndexMin = Infinity;
this.zIndexMax = -Infinity;
this.id = this.board.setId(this, 'V');
this.board.finalizeAdding(this);
this.elType = 'view3d';
};
JXG.View3D.prototype = new GeometryElement();
Type.copyMethodMap(JXG.View3D, {
// TODO
});
JXG.extend(
JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ {
/**
* Creates a new 3D element of type elementType.
* @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
* @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
* 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
* methods for a list of possible parameters.
* @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
* Common attributes are name, visible, strokeColor.
* @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
* two or more elements.
*/
create: function (elementType, parents, attributes) {
var prefix = [],
el;
if (elementType.indexOf('3d') > 0) {
// is3D = true;
prefix.push(this);
}
el = this.board.create(elementType, prefix.concat(parents), attributes);
return el;
},
/**
* Select a single or multiple elements at once.
* @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
* be used as a filter to return multiple elements at once filtered by the properties of the object.
* @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
* The advanced filters consisting of objects or functions are ignored.
* @returns {JXG.GeometryElement3D|JXG.Composition}
*
* @example
* // select the element with name A
* view.select('A');
*
* // select all elements with strokecolor set to 'red' (but not '#ff0000')
* view.select({
* strokeColor: 'red'
* });
*
* // select all points on or below the x/y plane and make them black.
* view.select({
* elType: 'point3d',
* Z: function (v) {
* return v <= 0;
* }
* }).setAttribute({color: 'black'});
*
* // select all elements
* view.select(function (el) {
* return true;
* });
*/
select: function (str, onlyByIdOrName) {
var flist,
olist,
i,
l,
s = str;
if (s === null) {
return s;
}
if (Type.isString(s) && s !== '') {
// It's a string, most likely an id or a name.
// Search by ID
if (Type.exists(this.objects[s])) {
s = this.objects[s];
// Search by name
} else if (Type.exists(this.elementsByName[s])) {
s = this.elementsByName[s];
// // Search by group ID
// } else if (Type.exists(this.groups[s])) {
// s = this.groups[s];
}
} else if (
!onlyByIdOrName &&
(Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
) {
// It's a function or an object, but not an element
flist = Type.filterElements(this.objectsList, s);
olist = {};
l = flist.length;
for (i = 0; i < l; i++) {
olist[flist[i].id] = flist[i];
}
s = new Composition(olist);
} else if (
Type.isObject(s) &&
Type.exists(s.id) &&
!Type.exists(this.objects[s.id])
) {
// It's an element which has been deleted (and still hangs around, e.g. in an attractor list)
s = null;
}
return s;
},
// set the Tait-Bryan angles to specify the current view rotation matrix
setAnglesFromRotation: function () {
var rem = this.matrix3DRot, // rotation remaining after angle extraction
rBank, cosBank, sinBank,
cosEl, sinEl,
cosAz, sinAz;
// extract bank by rotating the view box z axis onto the camera yz plane
rBank = Math.sqrt(rem[1][3] * rem[1][3] + rem[2][3] * rem[2][3]);
if (rBank > Mat.eps) {
cosBank = rem[2][3] / rBank;
sinBank = rem[1][3] / rBank;
} else {
// if the z axis is pointed almost exactly at the screen, we
// keep the current bank value
cosBank = Math.cos(this.angles.bank);
sinBank = Math.sin(this.angles.bank);
}
rem = Mat.matMatMult([
[1, 0, 0, 0],
[0, cosBank, -sinBank, 0],
[0, sinBank, cosBank, 0],
[0, 0, 0, 1]
], rem);
this.angles.bank = Math.atan2(sinBank, cosBank);
// extract elevation by rotating the view box z axis onto the camera
// y axis
cosEl = rem[2][3];
sinEl = rem[3][3];
rem = Mat.matMatMult([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, cosEl, sinEl],
[0, 0, -sinEl, cosEl]
], rem);
this.angles.el = Math.atan2(sinEl, cosEl);
// extract azimuth
cosAz = -rem[1][1];
sinAz = rem[3][1];
this.angles.az = Math.atan2(sinAz, cosAz);
if (this.angles.az < 0) this.angles.az += 2 * Math.PI;
this.setSlidersFromAngles();
},
anglesHaveMoved: function () {
return (
this._hasMoveAz || this._hasMoveEl ||
Math.abs(this.angles.az - this.az_slide.Value()) > Mat.eps ||
Math.abs(this.angles.el - this.el_slide.Value()) > Mat.eps ||
Math.abs(this.angles.bank - this.bank_slide.Value()) > Mat.eps
);
},
getAnglesFromSliders: function () {
this.angles.az = this.az_slide.Value();
this.angles.el = this.el_slide.Value();
this.angles.bank = this.bank_slide.Value();
},
setSlidersFromAngles: function () {
this.az_slide.setValue(this.angles.az);
this.el_slide.setValue(this.angles.el);
this.bank_slide.setValue(this.angles.bank);
},
// return the rotation matrix specified by the current Tait-Bryan angles
getRotationFromAngles: function () {
var a, e, b, f,
cosBank, sinBank,
mat = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
// mat projects homogeneous 3D coords in View3D
// to homogeneous 2D coordinates in the board
a = this.angles.az;
e = this.angles.el;
b = this.angles.bank;
f = -Math.sin(e);
mat[1][1] = -Math.cos(a);
mat[1][2] = Math.sin(a);
mat[1][3] = 0;
mat[2][1] = f * Math.sin(a);
mat[2][2] = f * Math.cos(a);
mat[2][3] = Math.cos(e);
mat[3][1] = Math.cos(e) * Math.sin(a);
mat[3][2] = Math.cos(e) * Math.cos(a);
mat[3][3] = Math.sin(e);
cosBank = Math.cos(b);
sinBank = Math.sin(b);
mat = Mat.matMatMult([
[1, 0, 0, 0],
[0, cosBank, sinBank, 0],
[0, -sinBank, cosBank, 0],
[0, 0, 0, 1]
], mat);
return mat;
/* this code, originally from `_updateCentralProjection`, is an
* alternate implementation of the azimuth-elevation matrix
* computation above. using this implementation instead of the
* current one might lead to simpler code in a future refactoring
var a, e, up,
ax, ay, az, v, nrm,
eye, d,
func_sphere;
// finds the point on the unit sphere with the given azimuth and
// elevation, and returns its affine coordinates
func_sphere = function (az, el) {
return [
Math.cos(az) * Math.cos(el),
-Math.sin(az) * Math.cos(el),
Math.sin(el)
];
};
a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
e = this.el_slide.Value();
// create an up vector and an eye vector which are 90 degrees out of phase
up = func_sphere(a, e + Math.PI / 2);
eye = func_sphere(a, e);
d = [eye[0], eye[1], eye[2]];
nrm = Mat.norm(d, 3);
az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
nrm = Mat.norm(up, 3);
v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
ax = Mat.crossProduct(v, az);
ay = Mat.crossProduct(az, ax);
this.matrix3DRot[1] = [0, ax[0], ax[1], ax[2]];
this.matrix3DRot[2] = [0, ay[0], ay[1], ay[2]];
this.matrix3DRot[3] = [0, az[0], az[1], az[2]];
*/
},
/**
* Project 2D point (x,y) to the virtual trackpad sphere,
* see Bell's virtual trackpad, and return z-component of the
* number.
*
* @param {Number} r
* @param {Number} x
* @param {Number} y
* @returns Number
* @private
*/
_projectToSphere: function (r, x, y) {
var d = Mat.hypot(x, y),
t, z;
if (d < r * 0.7071067811865475) { // Inside sphere
z = Math.sqrt(r * r - d * d);
} else { // On hyperbola
t = r / 1.414213562373095;
z = t * t / d;
}
return z;
},
/**
* Determine 4x4 rotation matrix with Bell's virtual trackball.
*
* @returns {Array} 4x4 rotation matrix
* @private
*/
updateProjectionTrackball: function (Pref) {
var R = 100,
dx, dy, dr2,
p1, p2, x, y, theta, t, d,
c, s, n,
mat = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
if (!Type.exists(this._trackball)) {
return this.matrix3DRot;
}
dx = this._trackball.dx;
dy = this._trackball.dy;
dr2 = dx * dx + dy * dy;
if (dr2 > Mat.eps) {
// // Method by Hanson, "The rolling ball", Graphics Gems III, p.51
// // Rotation axis:
// // n = (-dy/dr, dx/dr, 0)
// // Rotation angle around n:
// // theta = atan(dr / R) approx dr / R
// dr = Math.sqrt(dr2);
// c = R / Math.hypot(R, dr); // cos(theta)
// t = 1 - c; // 1 - cos(theta)
// s = dr / Math.hypot(R, dr); // sin(theta)
// n = [-dy / dr, dx / dr, 0];
// Bell virtual trackpad, see
// https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html
// http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c.
// See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited".
//
R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25;
x = this._trackball.x;
y = this._trackball.y;
p2 = [x, y, this._projectToSphere(R, x, y)];
x -= dx;
y -= dy;
p1 = [x, y, this._projectToSphere(R, x, y)];
n = Mat.crossProduct(p1, p2);
d = Mat.hypot(n[0], n[1], n[2]);
n[0] /= d;
n[1] /= d;
n[2] /= d;
t = Geometry.distance(p2, p1, 3) / (2 * R);
t = (t > 1.0) ? 1.0 : t;
t = (t < -1.0) ? -1.0 : t;
theta = 2.0 * Math.asin(t);
c = Math.cos(theta);
t = 1 - c;
s = Math.sin(theta);
// Rotation by theta about the axis n. See equation 9.63 of
//
// Ian Richard Cole. "Modeling CPV" (thesis). Loughborough
// University. https://hdl.handle.net/2134/18050
//
mat[1][1] = c + n[0] * n[0] * t;
mat[2][1] = n[1] * n[0] * t + n[2] * s;
mat[3][1] = n[2] * n[0] * t - n[1] * s;
mat[1][2] = n[0] * n[1] * t - n[2] * s;
mat[2][2] = c + n[1] * n[1] * t;
mat[3][2] = n[2] * n[1] * t + n[0] * s;
mat[1][3] = n[0] * n[2] * t + n[1] * s;
mat[2][3] = n[1] * n[2] * t - n[0] * s;
mat[3][3] = c + n[2] * n[2] * t;
}
mat = Mat.matMatMult(mat, this.matrix3DRot);
return mat;
},
updateAngleSliderBounds: function () {
var az_smax, az_smin,
el_smax, el_smin, el_cover,
el_smid, el_equiv, el_flip_equiv,
el_equiv_loss, el_flip_equiv_loss, el_interval_loss,
bank_smax, bank_smin;
// update stored trackball toggle
this.trackballEnabled = this.evalVisProp('trackball.enabled');
// set slider bounds
if (this.trackballEnabled) {
this.az_slide.setMin(0);
this.az_slide.setMax(2 * Math.PI);
this.el_slide.setMin(-0.5 * Math.PI);
this.el_slide.setMax(0.5 * Math.PI);
this.bank_slide.setMin(-Math.PI);
this.bank_slide.setMax(Math.PI);
} else {
this.az_slide.setMin(this.visProp.az.slider.min);
this.az_slide.setMax(this.visProp.az.slider.max);
this.el_slide.setMin(this.visProp.el.slider.min);
this.el_slide.setMax(this.visProp.el.slider.max);
this.bank_slide.setMin(this.visProp.bank.slider.min);
this.bank_slide.setMax(this.visProp.bank.slider.max);
}
// get new slider bounds
az_smax = this.az_slide._smax;
az_smin = this.az_slide._smin;
el_smax = this.el_slide._smax;
el_smin = this.el_slide._smin;
bank_smax = this.bank_slide._smax;
bank_smin = this.bank_slide._smin;
// wrap and restore angle values
if (this.trackballEnabled) {
// if we're upside-down, flip the bank angle to reach the same
// orientation with an elevation between -pi/2 and pi/2
el_cover = Mat.mod(this.angles.el, 2 * Math.PI);
if (0.5 * Math.PI < el_cover && el_cover < 1.5 * Math.PI) {
this.angles.el = Math.PI - el_cover;
this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
}
// wrap the azimuth and bank angle
this.angles.az = Mat.wrap(this.angles.az, az_smin, az_smax);
this.angles.el = Mat.wrap(this.angles.el, el_smin, el_smax);
this.angles.bank = Mat.wrap(this.angles.bank, bank_smin, bank_smax);
} else {
// wrap and clamp the elevation into the slider range. if
// flipping the elevation gets us closer to the slider interval,
// do that, inverting the azimuth and bank angle to compensate
el_interval_loss = function (t) {
if (t < el_smin) {
return el_smin - t;
} else if (el_smax < t) {
return t - el_smax;
} else {
return 0;
}
};
el_smid = 0.5 * (el_smin + el_smax);
el_equiv = Mat.wrap(
this.angles.el,
el_smid - Math.PI,
el_smid + Math.PI
);
el_flip_equiv = Mat.wrap(
Math.PI - this.angles.el,
el_smid - Math.PI,
el_smid + Math.PI
);
el_equiv_loss = el_interval_loss(el_equiv);
el_flip_equiv_loss = el_interval_loss(el_flip_equiv);
if (el_equiv_loss <= el_flip_equiv_loss) {
this.angles.el = Mat.clamp(el_equiv, el_smin, el_smax);
} else {
this.angles.el = Mat.clamp(el_flip_equiv, el_smin, el_smax);
this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
}
// wrap and clamp the azimuth and bank angle into the slider range
this.angles.az = Mat.wrapAndClamp(this.angles.az, az_smin, az_smax, 2 * Math.PI);
this.angles.bank = Mat.wrapAndClamp(this.angles.bank, bank_smin, bank_smax, 2 * Math.PI);
// since we're using `clamp`, angles may have changed
this.matrix3DRot = this.getRotationFromAngles();
}
// restore slider positions
this.setSlidersFromAngles();
},
/**
* Get distance from view box center to camera.
* In other words, the radius of the sphere where the camera sits.
* Distinguishes between projection tpye 'central' and 'parallel'.
* Uses the value of attribute 'r'.
*
* @returns Number
* @private
* @see View3D#r
*/
getCameraDistance: function() {
var rs, r, rr, diam;
rr = Type.evaluate(this.r);
if (rr === null || rr === 0) {
// Use attribute r
rs = this.evalVisProp('r');
} else {
// Use previously set value in this.r
rs = rr;
}
if (rs === 'auto') {
r = 1.01;
} else {
r = (this.projectionType === 'central') ? rs : (1 / rs);
}
if (this.projectionType === 'central') {
diam = Mat.hypot(
this.bbox3D[0][0] - this.bbox3D[0][1],
this.bbox3D[1][0] - this.bbox3D[1][1],
this.bbox3D[2][0] - this.bbox3D[2][1]
);
r = diam * r;
}
return r;
},
/**
* @private
* @returns {Array}
*/
_updateCentralProjection: function () {
var zf = 20, // near clip plane
zn = 8, // far clip plane
// See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
// bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
// All vectors contain affine coordinates and have length 3
// The matrices are of size 4x4.
r, A;
// Set distance from view box center to camera
r = this.getCameraDistance();
// Compute camera transformation
// this.boxToCam = this.matrix3DRot.map((row) => row.slice());
this.boxToCam = this.matrix3DRot.map(function (row) { return row.slice(); });
this.boxToCam[3][0] = -r;
// compute focal distance and clip space transformation
this.focalDist = 1 / Math.tan(0.5 * this.evalVisProp('fov'));
A = [
[0, 0, 0, -1],
[0, this.focalDist, 0, 0],
[0, 0, this.focalDist, 0],
[2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
];
return Mat.matMatMult(A, this.boxToCam);
},
// Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
// Called in board.updateElements()
update: function () {
var r, stretch,
mat2D, objectToClip, size,
dx, dy;
// objectsList;
if (
!Type.exists(this.el_slide) ||
!Type.exists(this.az_slide) ||
!Type.exists(this.bank_slide) ||
!this.needsUpdate
) {
this.needsUpdate = false;
return this;
}
mat2D = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
];
this.projectionType = this.evalVisProp('projection').toLowerCase();
// override angle slider bounds when trackball navigation is enabled
if (this.trackballEnabled !== this.evalVisProp('trackball.enabled')) {
this.updateAngleSliderBounds();
}
if (this._hasMoveTrackball) {
// The trackball has been moved since the last update, so we do
// trackball navigation. When the trackball is enabled, a drag
// event is interpreted as a trackball movement unless it's
// caught by something else, like point dragging. When the
// trackball is disabled, the trackball movement flag should
// never be set
this.matrix3DRot = this.updateProjectionTrackball();
this.setAnglesFromRotation();
} else if (this.anglesHaveMoved()) {
// The trackball hasn't been moved since the last up date, but
// the Tait-Bryan angles have been, so we do angle navigation
this.getAnglesFromSliders();
this.matrix3DRot = this.getRotationFromAngles();
}
/**
* The translation that moves the center of the view box to the origin.
*/
this.shift = [
[1, 0, 0, 0],
[-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
[-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
[-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
];
switch (this.projectionType) {
case 'central': // Central projection
// Add a final transformation to scale and shift the projection
// on the board, usually called viewport.
size = 2 * 0.4;
mat2D[1][1] = this.size[0] / size; // w / d_x
mat2D[2][2] = this.size[1] / size; // h / d_y
mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x
mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y
// The transformations this.matrix3D and mat2D can not be combined at this point,
// since the projected vectors have to be normalized in between in project3DTo2D
this.viewPortTransform = mat2D;
objectToClip = this._updateCentralProjection();
// this.matrix3D is a 4x4 matrix
this.matrix3D = Mat.matMatMult(objectToClip, this.shift);
break;
case 'parallel': // Parallel projection
default:
r = this.getCameraDistance();
stretch = [
[1, 0, 0, 0],
[0, r, 0, 0],
[0, 0, r, 0],
[0, 0, 0, r]
];
// Add a final transformation to scale and shift the projection
// on the board, usually called viewport.
dx = this.bbox3D[0][1] - this.bbox3D[0][0];
dy = this.bbox3D[1][1] - this.bbox3D[1][0];
mat2D[1][1] = this.size[0] / dx; // w / d_x
mat2D[2][2] = this.size[1] / dy; // h / d_y
mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x
mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y
// Combine all transformations, this.matrix3D is a 3x4 matrix
this.matrix3D = Mat.matMatMult(
mat2D,
Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3)
);
}
// Used for zIndex in dept ordering in subsequent update methods of the
// 3D elements and in view3d.updateRenderer
this.matrix3DRotShift = Mat.matMatMult(this.matrix3DRot, this.shift);
return this;
},
/**
* Compares 3D elements according to their z-Index.
* @param {JXG.GeometryElement3D} a
* @param {JXG.GeometryElement3D} b
* @returns Number
*/
compareDepth: function (a, b) {
// return a.zIndex - b.zIndex;
// if (a.type !== Const.OBJECT_TYPE_PLANE3D && b.type !== Const.OBJECT_TYPE_PLANE3D) {
// return a.zIndex - b.zIndex;
// } else if (a.type === Const.OBJECT_TYPE_PLANE3D) {
// let bHesse = Mat.innerProduct(a.point.coords, a.normal, 4);
// let po = Mat.innerProduct(b.coords, a.normal, 4);
// let pos = Mat.innerProduct(this.boxToCam[3], a.normal, 4);
// console.log(this.boxToCam[3])
// return pos - po;
// } else if (b.type === Const.OBJECT_TYPE_PLANE3D) {
// let bHesse = Mat.innerProduct(b.point.coords, b.normal, 4);
// let po = Mat.innerProduct(a.coords, a.normal, 4);
// let pos = Mat.innerProduct(this.boxToCam[3], b.normal, 4);
// console.log('b', pos, po, bHesse)
// return -pos;
// }
return a.zIndex - b.zIndex;
},
updateZIndices: function() {
var id, el;
for (id in this.objects) {
if (this.objects.hasOwnProperty(id)) {
el = this.objects[id];
// Update zIndex of less frequent objects line3d and polygon3d
// The other elements (point3d, face3d) do this in their update method.
if ((
el.type === Const.OBJECT_TYPE_LINE3D ||
el.type === Const.OBJECT_TYPE_POLYGON3D
) &&
Type.exists(el.element2D) &&
el.element2D.evalVisProp('visible')
) {
el.updateZIndex();
}
}
}
},
updateShaders: function() {
var id, el, v;
for (id in this.objects) {
if (this.objects.hasOwnProperty(id)) {
el = this.objects[id];
if (el.visPropCalc.visible && Type.exists(el.shader)) {
if (this.board._change3DView && el.evalVisProp('shader.fixed')) {
// In case, 3D view is rotated and the shader is fixed
// we can avoid the call of shader()
v = el.zIndex;
} else {
v = el.shader();
}
if (v < this.zIndexMin) {
this.zIndexMin = v;
} else if (v > this.zIndexMax) {
this.zIndexMax = v;
}
}
}
}
},
updateDepthOrdering: function () {
var id, el,
i, j, l, layers, lay;
// Collect elements for depth ordering layer-wise
layers = this.evalVisProp('depthorder.layers');
for (i = 0; i < layers.length; i++) {
this.depthOrdered[layers[i]] = [];
}
for (id in this.objects) {
if (this.objects.hasOwnProperty(id)) {
el = this.objects[id];
if ((el.type === Const.OBJECT_TYPE_FACE3D ||
el.type === Const.OBJECT_TYPE_LINE3D ||
// el.type === Const.OBJECT_TYPE_PLANE3D ||
el.type === Const.OBJECT_TYPE_POINT3D ||
el.type === Const.OBJECT_TYPE_POLYGON3D
) &&
Type.exists(el.element2D) &&
el.element2D.visPropCalc.visible
// el.element2D.evalVisProp('visible')
) {
lay = el.element2D.evalVisProp('layer');
if (layers.indexOf(lay) >= 0) {
this.depthOrdered[lay].push(el);
}
}
}
}
if (this.board.renderer && this.board.renderer.type === 'svg') {
for (i = 0; i < layers.length; i++) {
lay = layers[i];
this.depthOrdered[lay].sort(this.compareDepth.bind(this));
// DEBUG
// if (this.depthOrdered[lay].length > 0) {
// for (let k = 0; k < this.depthOrdered[lay].length; k++) {
// let o = this.depthOrdered[lay][k]
// console.log(o.visProp.fillcolor, o.zIndex)
// }
// }
l = this.depthOrdered[lay];
for (j = 0; j < l.length; j++) {
this.board.renderer.setLayer(l[j].element2D, lay);
}
// this.depthOrdered[lay].forEach((el) => this.board.renderer.setLayer(el.element2D, lay));
// Attention: forEach prevents deleting an element
}
}
return this;
},
updateRenderer: function () {
if (!this.needsUpdate) {
return this;
}
// console.time('update')
// Handle depth ordering
this.depthOrdered = {};
if (this.shift !== undefined && this.evalVisProp('depthorder.enabled')) {
// Update the zIndices of certain element types.
// We do it here in updateRenderer, because the elements' positions
// are meanwhile updated.
this.updateZIndices();
this.updateShaders();
if (this.board.renderer && this.board.renderer.type === 'svg') {
// For SVG we update the DOM order here.
// In canvas we sort the elements in board.updateRendererCanvas
this.updateDepthOrdering();
}
}
// console.timeEnd('update')
this.needsUpdate = false;
return this;
},
removeObject: function (object, saveMethod) {
var i, el, le, o, fst, face;
// this.board.removeObject(object, saveMethod);
if (Type.isArray(object)) {
for (i = 0; i < object.length; i++) {
this.removeObject(object[i]);
}
return this;
}
object = this.select(object);
// // If the object which is about to be removed unknown or a string, do nothing.
// // it is a string if a string was given and could not be resolved to an element.
if (!Type.exists(object) || Type.isString(object)) {
return this;
}
try {
// Remove all children.
for (el in object.childElements) {
if (object.childElements.hasOwnProperty(el)) {
this.removeObject(object.childElements[el]);
}
}
if (object.type === Const.OBJECT_TYPE_POLYHEDRON3D) {
// Special treatment for polyhedron3d.
// With this we can avoid the time consuming addChild() calls.
le = object.faces.length;
if (le > 0) {
fst = object.faces[0]._pos;
fst = (object.faces[0].element2D._pos < fst) ? object.faces[0].element2D._pos : fst;
}
for (i = 0; i < le; i++) {
face = object.faces[i];
delete this.objects[face.id];
// this.board.removeObject(face.element2D, saveMethod);
delete this.board.objects[face.element2D.id];
delete this.board.elementsByName[face.element2D.name];
face.element2D.remove();
this.board.objectsList.splice(face.element2D._pos, 1);
delete this.board.objects[face.id];
delete this.board.elementsByName[face.name];
face.remove();
this.board.objectsList.splice(face._pos, 1);
}
le = this.board.objectsList.length;
// Reindex the positions
for (i = fst; i < this.board.objectsList.length; i++) {
o = this.board.objectsList[i];
if (o._pos > -1) { o._pos = i; }
}
object.faces = [];
}
delete this.objects[object.id];
} catch (e) {
JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
}
// this.update();
this.board.removeObject(object, saveMethod);
return this;
},
/**
* Map world coordinates to focal coordinates. These coordinate systems
* are explained in the {@link JXG.View3D#boxToCam} matrix
* documentation.
*
* @param {Array} pWorld A world space point, in homogeneous coordinates.
* @param {Boolean} [homog=true] Whether to return homogeneous coordinates.
* If false, projects down to ordinary coordinates.
*/
worldToFocal: function (pWorld, homog = true) {
var k,
pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld));
pView[3] -= pView[0] * this.focalDist;
if (homog) {
return pView;
} else {
for (k = 1; k < 4; k++) {
pView[k] /= pView[0];
}
return pView.slice(1, 4);
}
},
/**
* Project 3D coordinates to 2D board coordinates
* The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
*
* @param {Number|Array} x
* @param {Number[]} y
* @param {Number[]} z
* @returns {Array} Array of length 3 containing the projection on to the board
* in homogeneous user coordinates.
*/
project3DTo2D: function (x, y, z) {
var vec, w;
if (arguments.length === 3) {
vec = [1, x, y, z];
} else {
// Argument is an array
if (x.length === 3) {
// vec = [1].concat(x);
vec = x.slice();
vec.unshift(1);
} else {
vec = x;
}
}
w = Mat.matVecMult(this.matrix3D, vec);
switch (this.projectionType) {
case 'central':
w[1] /= w[0];
w[2] /= w[0];
w[3] /= w[0];
w[0] /= w[0];
return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
case 'parallel':
default:
return w;
}
},
/**
* We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y.
* Setting R = mat^(-1) gives
* 1/ w0 * (1, x, y, d)^T = R * v2d.
* The first and the last row of this equation allows to determine 1/w0 and h.
*
* @param {Array} mat
* @param {Array} v2d
* @param {Number} d
* @returns Array
* @private
*/
_getW0: function (mat, v2d, d) {
var R = Mat.inverse(mat),
R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2],
R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2],
w, h, det;
det = d * R[0][3] - R[3][3];
w = (R2 * R[0][3] - R1 * R[3][3]) / det;
h = (R2 - R1 * d) / det;
return [1 / w, h];
},
/**
* Project a 2D coordinate to the plane defined by point "foot"
* and the normal vector `normal`.
*
* @param {Point} point2d
* @param {Array} normal Normal of plane
* @param {Array} foot Foot point of plane
* @returns {Array} of length 4 containing the projected
* point in homogeneous coordinates.
*/
project2DTo3DPlane: function (point2d, normal, foot) {
var mat, rhs, d, le, sol,
f = foot.slice(1) || [0, 0, 0],
n = normal.slice(1),
v2d, w0, res;
le = Mat.norm(n, 3);
d = Mat.innerProduct(f, n, 3) / le;
if (this.projectionType === 'parallel') {
mat = this.matrix3D.slice(0, 3); // Copy each row by reference
mat.push([0, n[0], n[1], n[2]]);
// 2D coordinates of point
rhs = point2d.coords.usrCoords.slice();
rhs.push(d);
try {
// Prevent singularity in case elevation angle is zero
if (mat[2][3] === 1.0) {
mat[2][1] = mat[2][2] = Mat.eps * 0.001;
}
sol = Mat.Numerics.Gauss(mat, rhs);
} catch (e) {
sol = [0, NaN, NaN, NaN];
}
} else {
mat = this.matrix3D;
// 2D coordinates of point:
rhs = point2d.coords.usrCoords.slice();
v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs);
res = this._getW0(mat, v2d, d);
w0 = res[0];
rhs = [
v2d[0] * w0,
v2d[1] * w0,
v2d[2] * w0,
res[1] * w0
];
try {
// Prevent singularity in case elevation angle is zero
if (mat[2][3] === 1.0) {
mat[2][1] = mat[2][2] = Mat.eps * 0.001;
}
sol = Mat.Numerics.Gauss(mat, rhs);
sol[1] /= sol[0];
sol[2] /= sol[0];
sol[3] /= sol[0];
// sol[3] = d;
sol[0] /= sol[0];
} catch (err) {
sol = [0, NaN, NaN, NaN];
}
}
return sol;
},
/**
* Project a point on the screen to the nearest point, in screen
* distance, on a line segment in 3d space. The inputs and outputs
* are in homogeneous coordinates.
*
* Used in View3d.project2DTo3DVertical() and
* Line3d.projectScreenCoords().
*
* @param {Array} pScr The screen coordinates of the point to project.
* @param {Array} end0 The world space coordinates of one end of the
* line segment (array of length 4).
* @param {Array} end1 The world space coordinates of the other end of
* the line segment (array of length 4).
*
* @returns {Array} Homogeneous coordinates of the projection
*/
projectScreenToSegment: function (pScr, end0, end1) {
var end0_2d = this.project3DTo2D(end0).slice(1, 3),
end1_2d = this.project3DTo2D(end1).slice(1, 3),
dir_2d = [
end1_2d[0] - end0_2d[0],
end1_2d[1] - end0_2d[1]
],
dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d),
diff = [
pScr[0] - end0_2d[0],
pScr[1] - end0_2d[1]
],
s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter
mid, mid_2d, mid_diff, m,
t, // view-space affine parameter
t_clamped, // affine parameter clamped to range
t_clamped_co;
if (this.projectionType === 'central') {
mid = [
1,
0.5 * (end0[1] + end1[1]),
0.5 * (end0[2] + end1[2]),
0.5 * (end0[3] + end1[3])
];
mid_2d = this.project3DTo2D(mid).slice(1, 3);
mid_diff = [
mid_2d[0] - end0_2d[0],
mid_2d[1] - end0_2d[1]
];
m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq;
// the view-space affine parameter s is related to the
// screen-space affine parameter t by a Möbius transformation,
// which is determined by the following relations:
//
// s | t
// -----
// 0 | 0
// m | 1/2
// 1 | 1
//
t = (1 - m) * s / ((1 - 2 * m) * s + m);
} else {
t = s;
}
t_clamped = Math.min(Math.max(t, 0), 1);
t_clamped_co = 1 - t_clamped;
return [
1,
t_clamped_co * end0[1] + t_clamped * end1[1],
t_clamped_co * end0[2] + t_clamped * end1[2],
t_clamped_co * end0[3] + t_clamped * end1[3]
];
},
/**
* Project a 2D coordinate to a new 3D position by keeping
* the 3D x, y coordinates and changing only the z coordinate.
* All horizontal moves of the 2D point are ignored.
*
* @param {Point} point2d
* @param {Array} base_c3d
* @returns {Array} of length 4 containing the projected
* point in homogeneous coordinates.
*/
project2DTo3DVertical: function (point2d, base_c3d) {
var pScr = point2d.coords.usrCoords.slice(1, 3),
end0 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][0]],
end1 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][1]];
return this.projectScreenToSegment(pScr, end0, end1);
},
/**
* Limit 3D coordinates to the bounding cube.
*
* @param {Array} c3d 3D coordinates [x,y,z]
* @returns Array [Array, Boolean] containing [coords, corrected]. coords contains the updated 3D coordinates,
* correct is true if the coords have been changed.
*/
project3DToCube: function (c3d) {
var cube = this.bbox3D,
isOut = false;
if (c3d[1] < cube[0][0]) {
c3d[1] = cube[0][0];
isOut = true;
}
if (c3d[1] > cube[0][1]) {
c3d[1] = cube[0][1];
isOut = true;
}
if (c3d[2] < cube[1][0]) {
c3d[2] = cube[1][0];
isOut = true;
}
if (c3d[2] > cube[1][1]) {
c3d[2] = cube[1][1];
isOut = true;
}
if (c3d[3] <= cube[2][0]) {
c3d[3] = cube[2][0];
isOut = true;
}
if (c3d[3] >= cube[2][1]) {
c3d[3] = cube[2][1];
isOut = true;
}
return [c3d, isOut];
},
/**
* Intersect a ray with the bounding cube of the 3D view.
* @param {Array} p 3D coordinates [w,x,y,z]
* @param {Array} dir 3D direction vector of the line (array of length 3 or 4)
* @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
* @returns Affine ratio of the intersection of the line with the cube.
*/
intersectionLineCube: function (p, dir, r) {
var r_n, i, r0, r1, d;
d = (dir.length === 3) ? dir : dir.slice(1);
r_n = r;
for (i = 0; i < 3; i++) {
if (d[i] !== 0) {
r0 = (this.bbox3D[i][0] - p[i + 1]) / d[i];
r1 = (this.bbox3D[i][1] - p[i + 1]) / d[i];
if (r < 0) {
r_n = Math.max(r_n, Math.min(r0, r1));
} else {
r_n = Math.min(r_n, Math.max(r0, r1));
}
}
}
return r_n;
},
/**
* Test if coordinates are inside of the bounding cube.
* @param {array} p 3D coordinates [[w],x,y,z] of a point.
* @returns Boolean
*/
isInCube: function (p, polyhedron) {
var q;
if (p.length === 4) {
if (p[0] === 0) {
return false;
}
q = p.slice(1);
}
return (
q[0] > this.bbox3D[0][0] - Mat.eps &&
q[0] < this.bbox3D[0][1] + Mat.eps &&
q[1] > this.bbox3D[1][0] - Mat.eps &&
q[1] < this.bbox3D[1][1] + Mat.eps &&
q[2] > this.bbox3D[2][0] - Mat.eps &&
q[2] < this.bbox3D[2][1] + Mat.eps
);
},
/**
*
* @param {Plane3D} plane1
* @param {Plane3D} plane2
* @param {Number} d Right hand side of Hesse normal for plane2 (it can be adjusted)
* @returns {Array} of length 2 containing the coordinates of the defining points of
* of the intersection segment, or false if there is no intersection
*/
intersectionPlanePlane: function (plane1, plane2, d) {
var ret = [false, false],
p, q, r, w,
dir;
d = d || plane2.d;
// Get one point of the intersection of the two planes
w = Mat.crossProduct(plane1.normal.slice(1), plane2.normal.slice(1));
w.unshift(0);
p = Mat.Geometry.meet3Planes(
plane1.normal,
plane1.d,
plane2.normal,
d,
w,
0
);
// Get the direction of the intersecting line of the two planes
dir = Mat.Geometry.meetPlanePlane(
plane1.vec1,
plane1.vec2,
plane2.vec1,
plane2.vec2
);
// Get the bounding points of the intersecting segment
r = this.intersectionLineCube(p, dir, Infinity);
q = Mat.axpy(r, dir, p);
if (this.isInCube(q)) {
ret[0] = q;
}
r = this.intersectionLineCube(p, dir, -Infinity);
q = Mat.axpy(r, dir, p);
if (this.isInCube(q)) {
ret[1] = q;
}
return ret;
},
intersectionPlaneFace: function (plane, face) {
var ret = [],
j, t,
p, crds,
p1, p2, c,
f, le, x1, y1, x2, y2,
dir, vec, w,
mat = [], b = [], sol;
w = Mat.crossProduct(plane.normal.slice(1), face.normal.slice(1));
w.unshift(0);
// Get one point of the intersection of the two planes
p = Geometry.meet3Planes(
plane.normal,
plane.d,
face.normal,
face.d,
w,
0
);
// Get the direction the intersecting line of the two planes
dir = Geometry.meetPlanePlane(
plane.vec1,
plane.vec2,
face.vec1,
face.vec2
);
f = face.polyhedron.faces[face.faceNumber];
crds = face.polyhedron.coords;
le = f.length;
for (j = 1; j <= le; j++) {
p1 = crds[f[j - 1]];
p2 = crds[f[j % le]];
vec = [0, p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
x1 = Math.random();
y1 = Math.random();
x2 = Math.random();
y2 = Math.random();
mat = [
[x1 * dir[1] + y1 * dir[3], x1 * (-vec[1]) + y1 * (-vec[3])],
[x2 * dir[2] + y2 * dir[3], x2 * (-vec[2]) + y2 * (-vec[3])]
];
b = [
x1 * (p1[1] - p[1]) + y1 * (p1[3] - p[3]),
x2 * (p1[2] - p[2]) + y2 * (p1[3] - p[3])
];
sol = Numerics.Gauss(mat, b);
t = sol[1];
if (t > -Mat.eps && t < 1 + Mat.eps) {
c = [1, p1[1] + t * vec[1], p1[2] + t * vec[2], p1[3] + t * vec[3]];
ret.push(c);
}
}
return ret;
},
// TODO:
// - handle non-closed polyhedra
// - handle intersections in vertex, edge, plane
intersectionPlanePolyhedron: function(plane, phdr) {
var i, j, seg,
p, first, pos, pos_akt,
eps = 1e-12,
points = [],
x = [],
y = [],
z = [];
for (i = 0; i < phdr.numberFaces; i++) {
if (phdr.def.faces[i].length < 3) {
// We skip intersection with points or lines
continue;
}
// seg will be an array consisting of two points
// that span the intersecting segment of the plane
// and the face.
seg = this.intersectionPlaneFace(plane, phdr.faces[i]);
// Plane intersects the face in less than 2 points
if (seg.length < 2) {
continue;
}
if (seg[0].length === 4 && seg[1].length === 4) {
// This test is necessary to filter out intersection lines which are
// identical to intersections of axis planes (they would occur twice),
// i.e. edges of bbox3d.
for (j = 0; j < points.length; j++) {
if (
(Geometry.distance(seg[0], points[j][0], 4) < eps &&
Geometry.distance(seg[1], points[j][1], 4) < eps) ||
(Geometry.distance(seg[0], points[j][1], 4) < eps &&
Geometry.distance(seg[1], points[j][0], 4) < eps)
) {
break;
}
}
if (j === points.length) {
points.push(seg.slice());
}
}
}
// Handle the case that the intersection is the empty set.
if (points.length === 0) {
return { X: x, Y: y, Z: z };
}
// Concatenate the intersection points to a polygon.
// If all went well, each intersection should appear
// twice in the list.
// __Attention:__ each face has to be planar!!!
// Otherwise the algorithm will fail.
first = 0;
pos = first;
i = 0;
do {
p = points[pos][i];
if (p.length === 4) {
x.push(p[1]);
y.push(p[2]);
z.push(p[3]);
}
i = (i + 1) % 2;
p = points[pos][i];
pos_akt = pos;
for (j = 0; j < points.length; j++) {
if (j !== pos && Geometry.distance(p, points[j][0]) < eps) {
pos = j;
i = 0;
break;
}
if (j !== pos && Geometry.distance(p, points[j][1]) < eps) {
pos = j;
i = 1;
break;
}
}
if (pos === pos_akt) {
console.log('Error face3d intersection update: did not find next', pos, i);
break;
}
} while (pos !== first);
x.push(x[0]);
y.push(y[0]);
z.push(z[0]);
return { X: x, Y: y, Z: z };
},
/**
* Generate mesh for a surface / plane.
* Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
* @param {Array|Function} func
* @param {Array} interval_u
* @param {Array} interval_v
* @returns Array
* @private
*
* @example
* var el = view.create('curve', [[], []]);
* el.updateDataArray = function () {
* var steps_u = this.evalVisProp('stepsu'),
* steps_v = this.evalVisProp('stepsv'),
* r_u = Type.evaluate(this.range_u),
* r_v = Type.evaluate(this.range_v),
* func, ret;
*
* if (this.F !== null) {
* func = this.F;
* } else {
* func = [this.X, this.Y, this.Z];
* }
* ret = this.view.getMesh(func,
* r_u.concat([steps_u]),
* r_v.concat([steps_v]));
*
* this.dataX = ret[0];
* this.dataY = ret[1];
* };
*
*/
getMesh: function (func, interval_u, interval_v) {
var i_u, i_v, u, v,
c2d, delta_u, delta_v,
p = [0, 0, 0],
steps_u = Type.evaluate(interval_u[2]),
steps_v = Type.evaluate(interval_v[2]),
dataX = [],
dataY = [];
delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
for (i_u = 0; i_u <= steps_u; i_u++) {
u = interval_u[0] + delta_u * i_u;
for (i_v = 0; i_v <= steps_v; i_v++) {
v = interval_v[0] + delta_v * i_v;
if (Type.isFunction(func)) {
p = func(u, v);
} else {
p = [func[0](u, v), func[1](u, v), func[2](u, v)];
}
c2d = this.project3DTo2D(p);
dataX.push(c2d[1]);
dataY.push(c2d[2]);
}
dataX.push(NaN);
dataY.push(NaN);
}
for (i_v = 0; i_v <= steps_v; i_v++) {
v = interval_v[0] + delta_v * i_v;
for (i_u = 0; i_u <= steps_u; i_u++) {
u = interval_u[0] + delta_u * i_u;
if (Type.isFunction(func)) {
p = func(u, v);
} else {
p = [func[0](u, v), func[1](u, v), func[2](u, v)];
}
c2d = this.project3DTo2D(p);
dataX.push(c2d[1]);
dataY.push(c2d[2]);
}
dataX.push(NaN);
dataY.push(NaN);
}
return [dataX, dataY];
},
/**
*
*/
animateAzimuth: function () {
var s = this.az_slide._smin,
e = this.az_slide._smax,
sdiff = e - s,
newVal = this.az_slide.Value() + 0.1;
this.az_slide.position = (newVal - s) / sdiff;
if (this.az_slide.position > 1) {
this.az_slide.position = 0.0;
}
this.board._change3DView = true;
this.board.update();
this.board._change3DView = false;
this.timeoutAzimuth = setTimeout(function () {
this.animateAzimuth();
}.bind(this), 200);
},
/**
*
*/
stopAzimuth: function () {
clearTimeout(this.timeoutAzimuth);
this.timeoutAzimuth = null;
},
/**
* Check if vertical dragging is enabled and which action is needed.
* Default is shiftKey.
*
* @returns Boolean
* @private
*/
isVerticalDrag: function () {
var b = this.board,
key;
if (!this.evalVisProp('verticaldrag.enabled')) {
return false;
}
key = '_' + this.evalVisProp('verticaldrag.key') + 'Key';
return b[key];
},
/**
* Stop ignoring attribute r. After a call of view3d.setView, view3d.nextView,
* view3d.previousView, or view3d.setCurrentView this attribute is ignored.
* Call of view3d.freeR() will end this.
* @see View3D#setView
*/
freeR: function() {
this.r = null;
},
/**
* Sets camera view to the given values.
* If the optional value r is supplied that value has priority until the next call of
* view3d.setView or until a call of view3d.freeR().
* In particular, the attribute r is ignored until a call of view3d.freeR().
* @param {Number} az Value of azimuth.
* @param {Number} el Value of elevation.
* @param {Number} [r] Value of radius.
*
* @returns {Object} Reference to the view.
* @see View3D#freeR
* @see View3D#r
* @see View3D#nextView
* @see View3D#previousView
* @see View3D#setCurrentView
*/
setView: function (az, el, r) {
// Set the distance to a fixed value.
if (r !== undefined) {
this.r = r;
}
r = this.getCameraDistance();
this.az_slide.setValue(az);
this.el_slide.setValue(el);
this.board.update();
return this;
},
/**
* Changes view to the next view stored in the attribute `values`.
*
* @see View3D#values
* @see View3D#setView
* @see View3D#previousView
* @see View3D#setCurrentView
* @see View3D#freeR
*
* @returns {Object} Reference to the view.
*/
nextView: function () {
var views = this.evalVisProp('values'),
n = this.visProp._currentview;
n = (n + 1) % views.length;
this.setCurrentView(n);
return this;
},
/**
* Changes view to the previous view stored in the attribute `values`.
*
* @see View3D#values
* @see View3D#setView
* @see View3D#nextView
* @see View3D#setCurrentView
* @see View3D#freeR
*
* @returns {Object} Reference to the view.
*/
previousView: function () {
var views = this.evalVisProp('values'),
n = this.visProp._currentview;
n = (n + views.length - 1) % views.length;
this.setCurrentView(n);
return this;
},
/**
* Changes view to the determined view stored in the attribute `values`.
*
* @see View3D#values
* @see View3D#nextView
* @see View3D#previousView
* @see View3D#setCurrentView
* @see View3D#freeR
*
* @param {Number} n Index of view in attribute `values`.
* @returns {Object} Reference to the view.
*/
setCurrentView: function (n) {
var views = this.evalVisProp('values');
if (n < 0 || n >= views.length) {
n = ((n % views.length) + views.length) % views.length;
}
this.setView(views[n][0], views[n][1], views[n][2]);
this.visProp._currentview = n;
return this;
},
/**
* Controls 2-degree navigation in az direction using pointer.
*
* @private
*
* @param {event} evt the pointer event
* @returns view
*/
_az_elEventHandler: function (evt) {
var smax = this.az_slide._smax,
smin = this.az_slide._smin,
speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')),
deltaX, // = evt.movementX,
deltaY, // = evt.movementY
az = this.az_slide.Value(),
el = this.el_slide.Value();
deltaX = evt.screenX - this._lastPos.x;
this._lastPos.x = evt.screenX;
deltaY = evt.screenY - this._lastPos.y;
this._lastPos.y = evt.screenY;
// Doesn't allow navigation if another moving event is triggered
if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
return this;
}
if (this.evalVisProp('az.pointer.enabled') && (deltaX !== 0) && evt.key == null) {
// delta *= (Math.abs(delta) > 100) ? 0.03 : 1;
az += deltaX * speed;
}
if (this.evalVisProp('el.pointer.enabled') && (deltaY !== 0) && evt.key == null) {
el += deltaY * speed;
}
// Project the calculated az value to a usable value in the interval [smin,smax]
// Use modulo if continuous is true
if (this.evalVisProp('az.continuous')) {
az = Mat.wrap(az, smin, smax);
} else {
if (az > 0) {
az = Math.min(smax, az);
} else if (az < 0) {
az = Math.max(smin, az);
}
}
// Project the calculated el value to a usable value in the interval [smin,smax]
// Use modulo if continuous is true and the trackball is disabled
smax = this.el_slide._smax;
smin = this.el_slide._smin;
if (this.evalVisProp('el.continuous') && !this.trackballEnabled) {
el = Mat.wrap(el, smin, smax);
} else {
if (el > 0) {
el = Math.min(smax, el);
} else if (el < 0) {
el = Math.max(smin, el);
}
}
this.setView(az, el);
return this;
},
/**
* Controls the navigation in az direction using either the keyboard or a pointer.
*
* @private
*
* @param {event} evt either the keydown or the pointer event
* @returns view
*/
_azEventHandler: function (evt) {
var smax = this.az_slide._smax,
smin = this.az_slide._smin,
speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')),
delta, // = evt.movementX,
az = this.az_slide.Value(),
el = this.el_slide.Value();
delta = evt.screenX - this._lastPos.x;
this._lastPos.x = evt.screenX;
// Doesn't allow navigation if another moving event is triggered
if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
return this;
}
// Calculate new az value if keyboard events are triggered
// Plus if right-button, minus if left-button
if (this.evalVisProp('az.keyboard.enabled')) {
if (evt.key === 'ArrowRight') {
az = az + this.evalVisProp('az.keyboard.step') * Math.PI / 180;
} else if (evt.key === 'ArrowLeft') {
az = az - this.evalVisProp('az.keyboard.step') * Math.PI / 180;
}
}
if (this.evalVisProp('az.pointer.enabled') && (delta !== 0) && evt.key == null) {
// delta *= (Math.abs(delta) > 100) ? 0.03 : 1;
az += delta * speed;
}
// Project the calculated az value to a usable value in the interval [smin,smax]
// Use modulo if continuous is true
if (this.evalVisProp('az.continuous')) {
az = Mat.wrap(az, smin, smax);
} else {
if (az > 0) {
az = Math.min(smax, az);
} else if (az < 0) {
az = Math.max(smin, az);
}
}
this.setView(az, el);
return this;
},
/**
* Controls the navigation in el direction using either the keyboard or a pointer.
*
* @private
*
* @param {event} evt either the keydown or the pointer event
* @returns view
*/
_elEventHandler: function (evt) {
var smax = this.el_slide._smax,
smin = this.el_slide._smin,
speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('el.pointer.speed'),
delta, // = evt.movementY,
az = this.az_slide.Value(),
el = this.el_slide.Value();
delta = evt.screenY - this._lastPos.y;
this._lastPos.y = evt.screenY;
// Doesn't allow navigation if another moving event is triggered
if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
return this;
}
// Calculate new az value if keyboard events are triggered
// Plus if down-button, minus if up-button
if (this.evalVisProp('el.keyboard.enabled')) {
if (evt.key === 'ArrowUp') {
el = el - this.evalVisProp('el.keyboard.step') * Math.PI / 180;
} else if (evt.key === 'ArrowDown') {
el = el + this.evalVisProp('el.keyboard.step') * Math.PI / 180;
}
}
if (this.evalVisProp('el.pointer.enabled') && (delta !== 0) && evt.key == null) {
// delta *= (Math.abs(delta) > 100) ? 0.05 : 1;
el += delta * speed;
}
// Project the calculated el value to a usable value in the interval [smin,smax]
// Use modulo if continuous is true and the trackball is disabled
if (this.evalVisProp('el.continuous') && !this.trackballEnabled) {
el = Mat.wrap(el, smin, smax);
} else {
if (el > 0) {
el = Math.min(smax, el);
} else if (el < 0) {
el = Math.max(smin, el);
}
}
this.setView(az, el);
return this;
},
/**
* Controls the navigation in bank direction using either the keyboard or a pointer.
*
* @private
*
* @param {event} evt either the keydown or the pointer event
* @returns view
*/
_bankEventHandler: function (evt) {
var smax = this.bank_slide._smax,
smin = this.bank_slide._smin,
step, speed,
delta = evt.deltaY, // Wheel event
bank = this.bank_slide.Value();
// Doesn't allow navigation if another moving event is triggered
if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
return this;
}
// Calculate new bank value if keyboard events are triggered
// Plus if down-button, minus if up-button
if (this.evalVisProp('bank.keyboard.enabled')) {
step = this.evalVisProp('bank.keyboard.step') * Math.PI / 180;
if (evt.key === '.' || evt.key === '<') {
bank -= step;
} else if (evt.key === ',' || evt.key === '>') {
bank += step;
}
}
if (this.evalVisProp('bank.pointer.enabled') && (delta !== 0) && evt.key == null) {
speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('bank.pointer.speed');
bank += delta * speed;
// prevent the pointer wheel from scrolling the page
evt.preventDefault();
}
// Project the calculated bank value to a usable value in the interval [smin,smax]
if (this.evalVisProp('bank.continuous')) {
// in continuous mode, wrap value around slider range
bank = Mat.wrap(bank, smin, smax);
} else {
// in non-continuous mode, clamp value to slider range
bank = Mat.clamp(bank, smin, smax);
}
this.bank_slide.setValue(bank);
this.board.update();
return this;
},
/**
* Controls the navigation using either virtual trackball.
*
* @private
*
* @param {event} evt either the keydown or the pointer event
* @returns view
*/
_trackballHandler: function (evt) {
var pos = this.board.getMousePosition(evt),
x, y, dx, dy, center;
center = new Coords(Const.COORDS_BY_USER, [this.llftCorner[0] + this.size[0] * 0.5, this.llftCorner[1] + this.size[1] * 0.5], this.board);
x = pos[0] - center.scrCoords[1];
y = pos[1] - center.scrCoords[2];
dx = evt.screenX - this._lastPos.x;
dy = evt.screenY - this._lastPos.y;
this._lastPos.x = evt.screenX;
this._lastPos.y = evt.screenY;
this._trackball = {
dx: dx,
dy: -dy,
x: x,
y: -y
};
this.board.update();
return this;
},
/**
* Event handler for pointer down event. Triggers handling of all 3D navigation.
*
* @private
* @param {event} evt
* @returns view
*/
pointerDownHandler: function (evt) {
var neededButton, neededKey, target;
this._hasMoveAzEl = false;
this._hasMoveAz = false;
this._hasMoveEl = false;
this._hasMoveBank = false;
this._hasMoveTrackball = false;
if (this.board.mode !== this.board.BOARD_MODE_NONE) {
return;
}
this.board._change3DView = true;
this._lastPos.x = evt.screenX;
this._lastPos.y = evt.screenY;
if (this.evalVisProp('trackball.enabled')) {
neededButton = this.evalVisProp('trackball.button');
neededKey = this.evalVisProp('trackball.key');
// Move events for virtual trackball
if (
(neededButton === -1 || neededButton === evt.button) &&
(neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
) {
// If outside is true then the event listener is bound to the document, otherwise to the div
target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj;
Env.addEvent(target, 'pointermove', this._trackballHandler, this);
this._hasMoveTrackball = true;
}
} else {
if (this.evalVisProp('az.pointer.enabled') && this.evalVisProp('el.pointer.enabled')) {
neededButton = this.evalVisProp('az.pointer.button');
neededKey = this.evalVisProp('az.pointer.key');
if (neededButton === this.evalVisProp('el.pointer.button') &&
neededKey === this.evalVisProp('el.pointer.key')) {
// Move events for azimuth and elevation
if (
(neededButton === -1 || neededButton === evt.button) &&
(neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) ||
(neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
) {
// If outside is true then the event listener is bound to the document, otherwise to the div
target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
if (target === ((this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj)) {
Env.addEvent(target, 'pointermove', this._az_elEventHandler, this);
this._hasMoveAzEl = true;
}
}
}
}
if (!this._hasMoveAzEl) {
if (this.evalVisProp('az.pointer.enabled')) {
neededButton = this.evalVisProp('az.pointer.button');
neededKey = this.evalVisProp('az.pointer.key');
// Move events for azimuth
if (
(neededButton === -1 || neededButton === evt.button) &&
(neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
) {
// If outside is true then the event listener is bound to the document, otherwise to the div
target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
Env.addEvent(target, 'pointermove', this._azEventHandler, this);
this._hasMoveAz = true;
}
}
if (this.evalVisProp('el.pointer.enabled')) {
neededButton = this.evalVisProp('el.pointer.button');
neededKey = this.evalVisProp('el.pointer.key');
// Events for elevation
if (
(neededButton === -1 || neededButton === evt.button) &&
(neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
) {
// If outside is true then the event listener is bound to the document, otherwise to the div
target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj;
Env.addEvent(target, 'pointermove', this._elEventHandler, this);
this._hasMoveEl = true;
}
}
}
if (this.evalVisProp('bank.pointer.enabled')) {
neededButton = this.evalVisProp('bank.pointer.button');
neededKey = this.evalVisProp('bank.pointer.key');
// Events for bank
if (
(neededButton === -1 || neededButton === evt.button) &&
(neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
) {
// If `outside` is true, we bind the event listener to
// the document. otherwise, we bind it to the div. we
// register the event listener as active so it can
// prevent the pointer wheel from scrolling the page
target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj;
Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false });
this._hasMoveBank = true;
}
}
}
Env.addEvent(document, 'pointerup', this.pointerUpHandler, this);
},
/**
* Event handler for pointer up event. Triggers handling of all 3D navigation.
*
* @private
* @param {event} evt
* @returns view
*/
pointerUpHandler: function (evt) {
var target;
if (this._hasMoveAzEl) {
target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
Env.removeEvent(target, 'pointermove', this._az_elEventHandler, this);
this._hasMoveAzEl = false;
}
if (this._hasMoveAz) {
target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
Env.removeEvent(target, 'pointermove', this._azEventHandler, this);
this._hasMoveAz = false;
}
if (this._hasMoveEl) {
target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj;
Env.removeEvent(target, 'pointermove', this._elEventHandler, this);
this._hasMoveEl = false;
}
if (this._hasMoveBank) {
target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj;
Env.removeEvent(target, 'wheel', this._bankEventHandler, this);
this._hasMoveBank = false;
}
if (this._hasMoveTrackball) {
target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj;
Env.removeEvent(target, 'pointermove', this._trackballHandler, this);
this._hasMoveTrackball = false;
}
Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this);
this.board._change3DView = false;
this.board.mode = this.board.BOARD_MODE_NONE;
}
});
/**
* @class A View3D element provides the container and the methods to create and display 3D elements.
*It is contained in a JSXGraph board.
*
* It is advisable to disable panning of the board by setting the board attribute "pan":
* ```
* pan: {enabled: false}
* ```
* Otherwise users will not be able to rotate the scene with their fingers on a touch device.
*
* The start position of the camera can be adjusted by the attributes {@link View3D#az}, {@link View3D#el}, and {@link View3D#bank}.
*
* @pseudo
* @name View3D
* @elementclass 3D
* @augments JXG.View3D
* @constructor
* @type Object
* @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
*/
/**
* @jsxgraphsignature View3D
* The arrays `[x, y]` and `[w, h]` define the 2D frame into which the 3D cube is
* (roughly) projected. If the view's `azimuth=0` and `elevation=0`, the 3D view will cover a rectangle with lower left corner
* `[x,y]` and side lengths `[w, h]` of the board.
* The array `cube` is of the form `[[x1, x2], [y1, y2], [z1, z2]]`
* which determines the coordinate ranges of the 3D cube.
* @param {Array} lower Array of the form `[x, y]`, position of the lower ledt corner of view3D in the board.
* @param {Array} dim Array of the form `[w, h]`. Size of view3D.
* @param {Array} cube 3D-size of view3D.
*
* @example
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'parallel',
* trackball: {enabled:true},
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* </pre><div id="JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b',
* {boundingbox: [-8, 8, 8,-8], pan: {enabled: false}, axis: false, showcopyright: false, shownavigation: false});
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'parallel',
* trackball: {enabled:true},
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* })();
*
* </script><pre>
*
* @example
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
* xPlaneRear: { visible: false },
* yPlaneRear: { visible: false }
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* </pre><div id="JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* xPlaneRear: { visible: false },
* yPlaneRear: { visible: false }
*
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* })();
*
* </script><pre>
*
* @example
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* // Main axes
* axesPosition: 'border',
*
* // Axes at the border
* xAxisBorder: { ticks3d: { ticksDistance: 2} },
* yAxisBorder: { ticks3d: { ticksDistance: 2} },
* zAxisBorder: { ticks3d: { ticksDistance: 2} },
*
* // No axes on planes
* xPlaneRearYAxis: {visible: false},
* xPlaneRearZAxis: {visible: false},
* yPlaneRearXAxis: {visible: false},
* yPlaneRearZAxis: {visible: false},
* zPlaneRearXAxis: {visible: false},
* zPlaneRearYAxis: {visible: false}
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* </pre><div id="JXG586f3551-335c-47e9-8d72-835409f6a103" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG586f3551-335c-47e9-8d72-835409f6a103',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* // Main axes
* axesPosition: 'border',
*
* // Axes at the border
* xAxisBorder: { ticks3d: { ticksDistance: 2} },
* yAxisBorder: { ticks3d: { ticksDistance: 2} },
* zAxisBorder: { ticks3d: { ticksDistance: 2} },
*
* // No axes on planes
* xPlaneRearYAxis: {visible: false},
* xPlaneRearZAxis: {visible: false},
* yPlaneRearXAxis: {visible: false},
* yPlaneRearZAxis: {visible: false},
* zPlaneRearXAxis: {visible: false},
* zPlaneRearYAxis: {visible: false}
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* })();
*
* </script><pre>
*
* @example
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
* axesPosition: 'none'
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* </pre><div id="JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* axesPosition: 'none'
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* })();
*
* </script><pre>
*
* @example
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* // Main axes
* axesPosition: 'border',
*
* // Axes at the border
* xAxisBorder: { ticks3d: { ticksDistance: 2} },
* yAxisBorder: { ticks3d: { ticksDistance: 2} },
* zAxisBorder: { ticks3d: { ticksDistance: 2} },
*
* xPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* yPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* zPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* xPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* yPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* zPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
*
* // No axes on planes
* xPlaneRearYAxis: {visible: false},
* xPlaneRearZAxis: {visible: false},
* yPlaneRearXAxis: {visible: false},
* yPlaneRearZAxis: {visible: false},
* zPlaneRearXAxis: {visible: false},
* zPlaneRearYAxis: {visible: false},
* xPlaneFrontYAxis: {visible: false},
* xPlaneFrontZAxis: {visible: false},
* yPlaneFrontXAxis: {visible: false},
* yPlaneFrontZAxis: {visible: false},
* zPlaneFrontXAxis: {visible: false},
* zPlaneFrontYAxis: {visible: false}
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
*
* </pre><div id="JXGbd41a4e3-1bf7-4764-b675-98b01667103b" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXGbd41a4e3-1bf7-4764-b675-98b01667103b',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var bound = [-4, 6];
* var view = board.create('view3d',
* [[-4, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* trackball: {enabled:true},
*
* // Main axes
* axesPosition: 'border',
*
* // Axes at the border
* xAxisBorder: { ticks3d: { ticksDistance: 2} },
* yAxisBorder: { ticks3d: { ticksDistance: 2} },
* zAxisBorder: { ticks3d: { ticksDistance: 2} },
*
* xPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* yPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* zPlaneRear: {
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* xPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* yPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
* zPlaneFront: {
* visible: true,
* fillColor: '#fff',
* mesh3d: {visible: false}
* },
*
* // No axes on planes
* xPlaneRearYAxis: {visible: false},
* xPlaneRearZAxis: {visible: false},
* yPlaneRearXAxis: {visible: false},
* yPlaneRearZAxis: {visible: false},
* zPlaneRearXAxis: {visible: false},
* zPlaneRearYAxis: {visible: false},
* xPlaneFrontYAxis: {visible: false},
* xPlaneFrontZAxis: {visible: false},
* yPlaneFrontXAxis: {visible: false},
* yPlaneFrontZAxis: {visible: false},
* zPlaneFrontXAxis: {visible: false},
* zPlaneFrontYAxis: {visible: false}
*
* });
*
* var curve = view.create('curve3d', [
* (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
* (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
* (t) => Math.sin(3 * t),
* [-Math.PI, Math.PI]
* ], { strokeWidth: 4 });
* })();
*
* </script><pre>
*
* @example
* var bound = [-5, 5];
* var view = board.create('view3d',
* [[-6, -3],
* [8, 8],
* [bound, bound, bound]],
* {
* // Main axes
* axesPosition: 'center',
* xAxis: { strokeColor: 'blue', strokeWidth: 3},
*
* // Planes
* xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}},
* yPlaneFront: { visible: true, fillColor: 'blue'},
*
* // Axes on planes
* xPlaneRearYAxis: {strokeColor: 'red'},
* xPlaneRearZAxis: {strokeColor: 'red'},
*
* yPlaneFrontXAxis: {strokeColor: 'blue'},
* yPlaneFrontZAxis: {strokeColor: 'blue'},
*
* zPlaneFrontXAxis: {visible: false},
* zPlaneFrontYAxis: {visible: false}
* });
*
* </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
* {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
* var bound = [-5, 5];
* var view = board.create('view3d',
* [[-6, -3], [8, 8],
* [bound, bound, bound]],
* {
* // Main axes
* axesPosition: 'center',
* xAxis: { strokeColor: 'blue', strokeWidth: 3},
* // Planes
* xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}},
* yPlaneFront: { visible: true, fillColor: 'blue'},
* // Axes on planes
* xPlaneRearYAxis: {strokeColor: 'red'},
* xPlaneRearZAxis: {strokeColor: 'red'},
* yPlaneFrontXAxis: {strokeColor: 'blue'},
* yPlaneFrontZAxis: {strokeColor: 'blue'},
* zPlaneFrontXAxis: {visible: false},
* zPlaneFrontYAxis: {visible: false}
* });
* })();
*
* </script><pre>
*
* @example
* var bound = [-5, 5];
* var view = board.create('view3d',
* [[-6, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* az: {
* slider: {
* visible: true,
* point1: {
* pos: [5, -4]
* },
* point2: {
* pos: [5, 4]
* },
* label: {anchorX: 'middle'}
* }
* },
* el: {
* slider: {
* visible: true,
* point1: {
* pos: [6, -5]
* },
* point2: {
* pos: [6, 3]
* },
* label: {anchorX: 'middle'}
* }
* },
* bank: {
* slider: {
* visible: true,
* point1: {
* pos: [7, -6]
* },
* point2: {
* pos: [7, 2]
* },
* label: {anchorX: 'middle'}
* }
* }
* });
*
*
* </pre><div id="JXGe181cc55-271b-419b-84fd-622326fd1d1a" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXGe181cc55-271b-419b-84fd-622326fd1d1a',
* {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
* var bound = [-5, 5];
* var view = board.create('view3d',
* [[-6, -3], [8, 8],
* [bound, bound, bound]],
* {
* projection: 'central',
* az: {
* slider: {
* visible: true,
* point1: {
* pos: [5, -4]
* },
* point2: {
* pos: [5, 4]
* },
* label: {anchorX: 'middle'}
* }
* },
* el: {
* slider: {
* visible: true,
* point1: {
* pos: [6, -5]
* },
* point2: {
* pos: [6, 3]
* },
* label: {anchorX: 'middle'}
* }
* },
* bank: {
* slider: {
* visible: true,
* point1: {
* pos: [7, -6]
* },
* point2: {
* pos: [7, 2]
* },
* label: {anchorX: 'middle'}
* }
* }
* });
*
*
* })();
*
* </script><pre>
*
*
*/
JXG.createView3D = function (board, parents, attributes) {
var view, attr, attr_az, attr_el, attr_bank,
x, y, w, h,
p1, p2, v,
coords = parents[0], // llft corner
size = parents[1]; // [w, h]
attr = Type.copyAttributes(attributes, board.options, 'view3d');
view = new JXG.View3D(board, parents, attr);
view.defaultAxes = view.create('axes3d', [], attr);
x = coords[0];
y = coords[1];
w = size[0];
h = size[1];
attr_az = Type.copyAttributes(attr, board.options, 'view3d', 'az', 'slider');
attr_az.name = 'az';
attr_el = Type.copyAttributes(attr, board.options, 'view3d', 'el', 'slider');
attr_el.name = 'el';
attr_bank = Type.copyAttributes(attr, board.options, 'view3d', 'bank', 'slider');
attr_bank.name = 'bank';
v = Type.evaluate(attr_az.point1.pos);
if (!Type.isArray(v)) {
// 'auto'
p1 = [x - 1, y - 2];
} else {
p1 = v;
}
v = Type.evaluate(attr_az.point2.pos);
if (!Type.isArray(v)) {
// 'auto'
p2 = [x + w + 1, y - 2];
} else {
p2 = v;
}
/**
* Slider to adapt azimuth angle
* @name JXG.View3D#az_slide
* @type {Slider}
*/
view.az_slide = board.create(
'slider',
[
p1, p2,
[
Type.evaluate(attr_az.min),
Type.evaluate(attr_az.start),
Type.evaluate(attr_az.max)
]
],
attr_az
);
view.inherits.push(view.az_slide);
view.az_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
v = Type.evaluate(attr_el.point1.pos);
if (!Type.isArray(v)) {
// 'auto'
p1 = [x - 1, y];
} else {
p1 = v;
}
v = Type.evaluate(attr_el.point2.pos);
if (!Type.isArray(v)) {
// 'auto'
p2 = [x - 1, y + h];
} else {
p2 = v;
}
/**
* Slider to adapt elevation angle
*
* @name JXG.View3D#el_slide
* @type {Slider}
*/
view.el_slide = board.create(
'slider',
[
p1, p2,
[
Type.evaluate(attr_el.min),
Type.evaluate(attr_el.start),
Type.evaluate(attr_el.max)]
],
attr_el
);
view.inherits.push(view.el_slide);
view.el_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
v = Type.evaluate(attr_bank.point1.pos);
if (!Type.isArray(v)) {
// 'auto'
p1 = [x - 1, y + h + 2];
} else {
p1 = v;
}
v = Type.evaluate(attr_bank.point2.pos);
if (!Type.isArray(v)) {
// 'auto'
p2 = [x + w + 1, y + h + 2];
} else {
p2 = v;
}
/**
* Slider to adjust bank angle
*
* @name JXG.View3D#bank_slide
* @type {Slider}
*/
view.bank_slide = board.create(
'slider',
[
p1, p2,
[
Type.evaluate(attr_bank.min),
Type.evaluate(attr_bank.start),
Type.evaluate(attr_bank.max)
]
],
attr_bank
);
view.inherits.push(view.bank_slide);
view.bank_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
// Set special infobox attributes of view3d.infobox
// Using setAttribute() is not possible here, since we have to
// avoid a call of board.update().
// The drawback is that we can not use shortcuts
view.board.infobox.visProp = Type.merge(view.board.infobox.visProp, attr.infobox);
// 3d infobox: drag direction and coordinates
view.board.highlightInfobox = function (x, y, el) {
var d, i, c3d, foot,
pre = '',
brd = el.board,
arr, infobox,
p = null;
if (this.mode === this.BOARD_MODE_DRAG) {
// Drag direction is only shown during dragging
if (view.isVerticalDrag()) {
pre = '<span style="color:black; font-size:200%">\u21C5 </span>';
} else {
pre = '<span style="color:black; font-size:200%">\u21C4 </span>';
}
}
// Search 3D parent
for (i = 0; i < el.parents.length; i++) {
p = brd.objects[el.parents[i]];
if (p.is3D) {
break;
}
}
if (p && Type.exists(p.element2D)) {
foot = [1, 0, 0, p.coords[3]];
view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4);
c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot);
if (!view.isInCube(c3d)) {
view.board.highlightCustomInfobox('', p);
return;
}
d = p.evalVisProp('infoboxdigits');
infobox = view.board.infobox;
if (d === 'auto') {
if (infobox.useLocale()) {
arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')'];
} else {
arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')'];
}
} else {
if (infobox.useLocale()) {
arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')'];
} else {
arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')'];
}
}
view.board.highlightCustomInfobox(arr.join(''), p);
} else {
view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
}
};
// Hack needed to enable addEvent for view3D:
view.BOARD_MODE_NONE = 0x0000;
// Add events for the keyboard navigation
Env.addEvent(board.containerObj, 'keydown', function (event) {
var neededKey,
catchEvt = false;
// this.board._change3DView = true;
if (view.evalVisProp('el.keyboard.enabled') &&
(event.key === 'ArrowUp' || event.key === 'ArrowDown')
) {
neededKey = view.evalVisProp('el.keyboard.key');
if (neededKey === 'none' ||
(neededKey.indexOf('shift') > -1 && event.shiftKey) ||
(neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
view._elEventHandler(event);
catchEvt = true;
}
}
if (view.evalVisProp('az.keyboard.enabled') &&
(event.key === 'ArrowLeft' || event.key === 'ArrowRight')
) {
neededKey = view.evalVisProp('az.keyboard.key');
if (neededKey === 'none' ||
(neededKey.indexOf('shift') > -1 && event.shiftKey) ||
(neededKey.indexOf('ctrl') > -1 && event.ctrlKey)
) {
view._azEventHandler(event);
catchEvt = true;
}
}
if (view.evalVisProp('bank.keyboard.enabled') && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) {
neededKey = view.evalVisProp('bank.keyboard.key');
if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
view._bankEventHandler(event);
catchEvt = true;
}
}
if (event.key === 'PageUp') {
view.nextView();
catchEvt = true;
} else if (event.key === 'PageDown') {
view.previousView();
catchEvt = true;
}
if (catchEvt) {
// We stop event handling only in the case if the keypress could be
// used for the 3D view. If this is not done, input fields et al
// can not be used any more.
event.preventDefault();
}
this.board._change3DView = false;
}, view);
// Add events for the pointer navigation
Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view);
// Initialize view rotation matrix
view.getAnglesFromSliders();
view.matrix3DRot = view.getRotationFromAngles();
// override angle slider bounds when trackball navigation is enabled
view.updateAngleSliderBounds();
view.board.update();
return view;
};
JXG.registerElement("view3d", JXG.createView3D);
export default JXG.View3D;