/*
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 Type from "../utils/type.js";
import Mat from "../math/math.js";
/**
* @class Creates a new 3D face object. 3D faces are part of a {@link JXG.Polyhedron3D}. Do not use this constructor to create a 3D face.
*
* @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.Face3D = function (view, polyhedron, faceNumber, attributes) {
this.constructor(view.board, attributes, Const.OBJECT_TYPE_FACE3D, Const.OBJECT_CLASS_3D);
this.constructor3D(view, 'face3d');
this.board.finalizeAdding(this);
/**
* Link to the defining data of the parent polyhedron3d.
* @name Face3D#polyhedron
* @type Object
* @see Polyhedron3D#def
*/
this.polyhedron = polyhedron;
/**
* Index of the face in the list of faces of the polyhedron
* @name Face3D#faceNumber
* @type Number
*/
this.faceNumber = faceNumber;
/**
* Normal vector for the face. Array of length 4.
* @name Face3D#normal
* @type array
*/
this.normal = [0, 0, 0, 0];
/**
* Hesse right hand side of the plane that contains the face.
* @name Face3D#d
* @type Number
*/
this.d = 0;
/**
* First basis vector of the face. Vector of length 4.
* @name Face3D#vec1
* @type Array
*/
this.vec1 = [0, 0, 0, 0];
/**
* Second basis vector of the face. Vector of length 4.
* @name Face3D#vec2
* @type Array
*/
this.vec2 = [0, 0, 0, 0];
if (this.faceNumber === 0) {
this.updateCoords();
}
};
JXG.Face3D.prototype = new JXG.GeometryElement();
Type.copyPrototypeMethods(JXG.Face3D, JXG.GeometryElement3D, 'constructor3D');
Type.copyMethodMap(JXG.Face3D, {
// TODO
});
JXG.extend(
JXG.Face3D.prototype,
/** @lends JXG.Face3D.prototype */ {
/**
* Update the coordinates of ALL vertices of the polyhedron.
* @function
* @name Face3D#updateCoords
* @returns {Face3D} reference to itself
* @see Polyhedron3D#def
*/
updateCoords: function() {
var i, j, le, p,
def = this.polyhedron;
for (i in def.vertices) {
p = def.vertices[i];
if (Type.isFunction(p)) {
def.coords[i] = Type.evaluate(p);
} else if (Type.isArray(p)) {
def.coords[i] = [];
le = p.length;
for (j = 0; j < le; j++) {
def.coords[i][j] = Type.evaluate(p[j]);
}
} else {
p = def.view.select(p);
if (Type.isPoint3D(p)) {
def.coords[i] = p.coords;
} else {
throw new Error('Polyhedron3D.updateCoords: unknown vertices type!');
}
}
if (def.coords[i].length === 3) {
def.coords[i].unshift(1);
}
}
return this;
},
/**
* Update the 2D coordinates of the face and determine it's z-index.
* @function
* @name Face3D#updateDataArray2D
* @returns {Object} {X:[], Y:[]}
*/
updateDataArray2D: function () {
var j, le,
c3d, c2d,
x = [],
y = [],
p = this.polyhedron,
face = p.faces[this.faceNumber];
if (this.faceNumber === 0) {
// coords2D equal to [] means, projection is needed down below.
// Thus, every vertex is projected only once.
for (j in p.vertices) {
p.coords2D[j] = [];
}
}
// Add the projected coordinates of the vertices of this face
// to the 2D curve.
// If not done yet, project the 3D vertices of this face to 2D.
le = face.length;
this.zIndex = 0.0;
for (j = 0; j < le; j++) {
c2d = p.coords2D[face[j]];
if (c2d.length === 0) {
// if coords2D.length > 0, it has already be projected
// in another face3d.
c3d = p.coords[face[j]];
c2d = this.view.project3DTo2D(c3d);
p.coords2D[face[j]] = c2d;
// p.zIndex[face[j]] = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3];
p.zIndex[face[j]] = Mat.innerProduct(this.view.matrix3DRotShift[3], c3d);
}
x.push(c2d[1]);
y.push(c2d[2]);
this.zIndex += p.zIndex[face[j]];
}
if (le > 0) {
this.zIndex /= le;
}
if (le !== 2) {
// 2D faces and points are a closed loop
x.push(x[0]);
y.push(y[0]);
}
return { X: x, Y: y };
},
addTransform: function (el, transform) {
if (this.faceNumber === 0) {
this.addTransformGeneric(el, transform);
}
return this;
},
removeTransform: function (transform) {
if (this.faceNumber === 0) {
this.removeTransformGeneric(transform);
}
return this;
},
clearTransforms: function () {
if (this.faceNumber === 0) {
this.clearTransformsGeneric();
}
return this;
},
updateTransform: function () {
var t, c, i, j, b;
if (this.faceNumber !== 0) {
return this;
}
if (this.transformations.length === 0 || this.baseElement === null) {
return this;
}
t = this.transformations;
for (i = 0; i < t.length; i++) {
t[i].update();
}
if (this === this.baseElement) {
b = this.polyhedron;
} else {
b = this.baseElement.polyhedron;
}
for (i in b.coords) {
if (b.coords.hasOwnProperty(i)) {
c = b.coords[i];
for (j = 0; j < t.length; j++) {
c = Mat.matVecMult(t[j].matrix, c);
}
this.polyhedron.coords[i] = c;
}
}
return this;
},
update: function () {
var i, le,
phdr, nrm,
p1, p2,
face;
if (this.needsUpdate && !this.view.board._change3DView) {
// Do not update face coordinates and normal during view rotation
phdr = this.polyhedron;
if (this.faceNumber === 0) {
// Update coordinates of all vertices
this.updateCoords()
.updateTransform();
}
face = phdr.faces[this.faceNumber];
le = face.length;
if (le < 3) {
// Get out of here if face is point or segment
return this;
}
// Update spanning vectors
p1 = phdr.coords[face[0]];
p2 = phdr.coords[face[1]];
this.vec1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
p2 = phdr.coords[face[2]];
this.vec2 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
// Update Hesse form, i.e. normal and d
this.normal = Mat.crossProduct(this.vec1.slice(1), this.vec2.slice(1));
nrm = Mat.norm(this.normal);
this.normal.unshift(0);
if (Math.abs(nrm) > 1.e-12) {
for (i = 1; i < 4; i++) {
this.normal[i] /= nrm;
}
}
this.d = Mat.innerProduct(p1, this.normal, 4);
}
return this;
},
updateRenderer: function () {
if (this.needsUpdate) {
this.needsUpdate = false;
}
return this;
},
// To be merged with View3d.getRotationFromAngles
getRotationFromAngles: function (angles) {
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 = angles.az;
e = angles.el;
b = 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;
},
/**
* Determines the lightness of the face (in the HSL color scheme).
*
* Sets the fillColor of the adjoint 2D curve.
* @name shader
* @memberOf Face3D
* @function
* @returns {Number} zIndex of the face
*/
shader: function() {
var hue, sat, light, angle, hsl,
sun, angles,
abs,
lightObj,
minFace, maxFace,
minLight, maxLight;
if (this.evalVisProp('shader.enabled')) {
hue = this.evalVisProp('shader.hue');
sat = this.evalVisProp('shader.saturation');
minLight = this.evalVisProp('shader.minlightness');
maxLight = this.evalVisProp('shader.maxlightness');
if (this.evalVisProp('shader.type').toLowerCase() === 'angle') {
lightObj = this.evalVisProp('shader.light');
switch (lightObj.type) {
// 1: lighting==camera (default),
// 2: Fixed: angle(object, camera),
// 3: Fixed: angle(lighting, camera)
case 2: // Fixed: angle(object, camera)
angles = {
az: lightObj.az * Math.PI / 180,
el: lightObj.el * Math.PI / 180,
bank: lightObj.bank * Math.PI / 180
};
sun = this.getRotationFromAngles(angles)[3];
abs = lightObj.dir;
break;
case 3: // Fixed: angle(lighting, camera)
angles = {
az: this.view.angles.az + lightObj.az * Math.PI / 180,
el: this.view.angles.el + lightObj.el * Math.PI / 180,
bank: this.view.angles.bank
};
sun = this.getRotationFromAngles(angles)[3];
abs = lightObj.dir;
break;
default: // Fixed: angle(lighting, camera) = 0
sun = this.view.matrix3DRotShift[3];
abs = lightObj.dir;
}
// angle = Mat.innerProduct(this.view.matrix3DRotShift[3], this.normal);
angle = Mat.innerProduct(sun, this.normal);
angle = (abs === 0) ? Math.abs(angle) : ((abs < 0) ? -angle : angle);
light = minLight + (maxLight - minLight) * angle;
} else {
// zIndex
maxFace = this.view.zIndexMax;
minFace = this.view.zIndexMin;
light = minLight + (maxLight - minLight) * ((this.zIndex - minFace) / (maxFace - minFace));
}
// hsl = `hsl(${hue}, ${sat}%, ${light}%)`;
hsl = 'hsl(' + hue + ',' + sat +'%,' + light + '%)';
this.element2D.visProp.fillcolor = hsl;
this._shadingDone = true;
return this.zIndex;
}
}
}
);
/**
* @class This element creates a 3D face.
* This is not a standalone object, but a part of a {@link Polyhedron3D}.
*
* @pseudo
* @name Face3D
* @elementclass 3D
* @augments JXG.Curve
* @constructor
* @type Object
* @private
* @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
*/
JXG.createFace3D = function (board, parents, attributes) {
var view = parents[0],
polyhedron = parents[1],
faceNumber = parents[2],
attr, el;
// TODO Throw new Error
attr = Type.copyAttributes(attributes, board.options, 'face3d');
el = new JXG.Face3D(view, polyhedron, faceNumber, attr);
attr = el.setAttr2D(attr);
el.element2D = view.create("curve", [[], []], attr);
el.element2D.view = view;
el.element2D.dump = false;
/**
* @class
* @ignore
*/
el.element2D.updateDataArray = function () {
var ret = el.updateDataArray2D();
this.dataX = ret.X;
this.dataY = ret.Y;
};
// Deactivate hasPoint to increase speed.
// This is be too agressive, e.g. for the upcoming
// rotation3D circles (SoftwarePraktikum)
// We may enable have to enable hasPoint() for
// click events.
el.element2D.hasPoint = function() {};
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("face3d", JXG.createFace3D);