playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
364 lines (361 loc) • 13.3 kB
JavaScript
import { EventHandler } from '../../core/event-handler.js';
import { Color } from '../../core/math/color.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Vec4 } from '../../core/math/vec4.js';
import { COLOR_RED, COLOR_GREEN, COLOR_BLUE } from './color.js';
const tmpV1 = new Vec3();
const tmpV2 = new Vec3();
const tmpV3 = new Vec3();
const tmpM1 = new Mat4();
class ViewCube extends EventHandler {
set anchor(value) {
this._anchor.copy(value);
this.dom.style.top = this._anchor.x ? '0px' : 'auto';
this.dom.style.right = this._anchor.y ? '0px' : 'auto';
this.dom.style.bottom = this._anchor.z ? '0px' : 'auto';
this.dom.style.left = this._anchor.w ? '0px' : 'auto';
}
get anchor() {
return this._anchor;
}
/**
* @type {Color}
*/ set colorX(value) {
this._colorX.copy(value);
this._shapes.px.children[0].setAttribute('fill', this._colorX.toString(false));
this._shapes.px.children[0].setAttribute('stroke', this._colorX.toString(false));
this._shapes.nx.children[0].setAttribute('stroke', this._colorX.toString(false));
this._shapes.xaxis.setAttribute('stroke', this._colorX.toString(false));
}
get colorX() {
return this._colorX;
}
/**
* @type {Color}
*/ set colorY(value) {
this._colorY.copy(value);
this._shapes.py.children[0].setAttribute('fill', this._colorY.toString(false));
this._shapes.py.children[0].setAttribute('stroke', this._colorY.toString(false));
this._shapes.ny.children[0].setAttribute('stroke', this._colorY.toString(false));
this._shapes.yaxis.setAttribute('stroke', this._colorY.toString(false));
}
get colorY() {
return this._colorY;
}
/**
* @type {Color}
*/ set colorZ(value) {
this._colorZ.copy(value);
this._shapes.pz.children[0].setAttribute('fill', this._colorZ.toString(false));
this._shapes.pz.children[0].setAttribute('stroke', this._colorZ.toString(false));
this._shapes.nz.children[0].setAttribute('stroke', this._colorZ.toString(false));
this._shapes.zaxis.setAttribute('stroke', this._colorZ.toString(false));
}
get colorZ() {
return this._colorZ;
}
/**
* @type {Color}
*/ set colorNeg(value) {
this._colorNeg.copy(value);
this._shapes.px.children[0].setAttribute('fill', this._colorNeg.toString(false));
this._shapes.py.children[0].setAttribute('fill', this._colorNeg.toString(false));
this._shapes.pz.children[0].setAttribute('fill', this._colorNeg.toString(false));
}
get colorNeg() {
return this._colorNeg;
}
/**
* @type {number}
*/ set radius(value) {
this._radius = value;
this._shapes.px.children[0].setAttribute('r', `${value}`);
this._shapes.py.children[0].setAttribute('r', `${value}`);
this._shapes.pz.children[0].setAttribute('r', `${value}`);
this._shapes.nx.children[0].setAttribute('r', `${value}`);
this._shapes.ny.children[0].setAttribute('r', `${value}`);
this._shapes.nz.children[0].setAttribute('r', `${value}`);
this._resize();
}
get radius() {
return this._radius;
}
/**
* @type {number}
*/ set textSize(value) {
this._textSize = value;
this._shapes.px.children[1].setAttribute('font-size', `${value}`);
this._shapes.py.children[1].setAttribute('font-size', `${value}`);
this._shapes.pz.children[1].setAttribute('font-size', `${value}`);
}
get textSize() {
return this._textSize;
}
/**
* @type {number}
*/ set lineThickness(value) {
this._lineThickness = value;
this._shapes.xaxis.setAttribute('stroke-width', `${value}`);
this._shapes.yaxis.setAttribute('stroke-width', `${value}`);
this._shapes.zaxis.setAttribute('stroke-width', `${value}`);
this._shapes.px.children[0].setAttribute('stroke-width', `${value}`);
this._shapes.py.children[0].setAttribute('stroke-width', `${value}`);
this._shapes.pz.children[0].setAttribute('stroke-width', `${value}`);
this._shapes.nx.children[0].setAttribute('stroke-width', `${value}`);
this._shapes.ny.children[0].setAttribute('stroke-width', `${value}`);
this._shapes.nz.children[0].setAttribute('stroke-width', `${value}`);
this._resize();
}
get lineThickness() {
return this._lineThickness;
}
/**
* @type {number}
*/ set lineLength(value) {
this._lineLength = value;
this._resize();
}
get lineLength() {
return this._lineLength;
}
/**
* @private
*/ _resize() {
this._size = 2 * (this.lineLength + this.radius + this.lineThickness);
this.dom.style.width = `${this._size}px`;
this.dom.style.height = `${this._size}px`;
this._svg.setAttribute('width', `${this._size}`);
this._svg.setAttribute('height', `${this._size}`);
this._group.setAttribute('transform', `translate(${this._size * 0.5}, ${this._size * 0.5})`);
}
/**
* @private
* @param {SVGAElement} group - The group.
* @param {number} x - The x.
* @param {number} y - The y.
*/ _transform(group, x, y) {
group.setAttribute('transform', `translate(${x * this._lineLength}, ${y * this._lineLength})`);
}
/**
* @private
* @param {SVGLineElement} line - The line.
* @param {number} x - The x.
* @param {number} y - The y.
*/ _x2y2(line, x, y) {
line.setAttribute('x2', `${x * this._lineLength}`);
line.setAttribute('y2', `${y * this._lineLength}`);
}
/**
* @private
* @param {string} color - The color.
* @returns {SVGLineElement} - The line.
*/ _line(color) {
const result = /** @type {SVGLineElement} */ document.createElementNS(this._svg.namespaceURI, 'line');
result.setAttribute('stroke', color);
result.setAttribute('stroke-width', `${this._lineThickness}`);
this._group.appendChild(result);
return result;
}
/**
* @private
* @param {string} color - The color.
* @param {boolean} [fill] - The fill.
* @param {string} [text] - The text.
* @returns {SVGAElement} - The circle.
*/ _circle(color, fill = false, text) {
const group = /** @type {SVGAElement} */ document.createElementNS(this._svg.namespaceURI, 'g');
const circle = /** @type {SVGCircleElement} */ document.createElementNS(this._svg.namespaceURI, 'circle');
circle.setAttribute('fill', fill ? color : this._colorNeg.toString(false));
circle.setAttribute('stroke', color);
circle.setAttribute('stroke-width', `${this._lineThickness}`);
circle.setAttribute('r', `${this._radius}`);
circle.setAttribute('cx', '0');
circle.setAttribute('cy', '0');
circle.setAttribute('pointer-events', 'all');
group.appendChild(circle);
if (text) {
const t = /** @type {SVGTextElement} */ document.createElementNS(this._svg.namespaceURI, 'text');
t.setAttribute('font-size', `${this._textSize}`);
t.setAttribute('font-family', 'Arial');
t.setAttribute('font-weight', 'bold');
t.setAttribute('text-anchor', 'middle');
t.setAttribute('alignment-baseline', 'central');
t.textContent = text;
group.appendChild(t);
}
group.setAttribute('cursor', 'pointer');
this._group.appendChild(group);
return group;
}
/**
* @param {Mat4} cameraMatrix - The camera matrix.
*/ update(cameraMatrix) {
// skip if the container is not visible
if (!this._size) {
return;
}
tmpM1.invert(cameraMatrix);
tmpM1.getX(tmpV1);
tmpM1.getY(tmpV2);
tmpM1.getZ(tmpV3);
this._transform(this._shapes.px, tmpV1.x, -tmpV1.y);
this._transform(this._shapes.nx, -tmpV1.x, tmpV1.y);
this._transform(this._shapes.py, tmpV2.x, -tmpV2.y);
this._transform(this._shapes.ny, -tmpV2.x, tmpV2.y);
this._transform(this._shapes.pz, tmpV3.x, -tmpV3.y);
this._transform(this._shapes.nz, -tmpV3.x, tmpV3.y);
this._x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y);
this._x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y);
this._x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y);
// reorder dom for the mighty svg painter's algorithm
const order = [
{
n: [
'xaxis',
'px'
],
value: tmpV1.z
},
{
n: [
'yaxis',
'py'
],
value: tmpV2.z
},
{
n: [
'zaxis',
'pz'
],
value: tmpV3.z
},
{
n: [
'nx'
],
value: -tmpV1.z
},
{
n: [
'ny'
],
value: -tmpV2.z
},
{
n: [
'nz'
],
value: -tmpV3.z
}
].sort((a, b)=>a.value - b.value);
const fragment = document.createDocumentFragment();
order.forEach((o)=>{
o.n.forEach((n)=>{
fragment.appendChild(this._shapes[n]);
});
});
this._group.appendChild(fragment);
}
destroy() {
this.dom.remove();
this.off();
}
/**
* @param {Vec4} [anchor] - The anchor.
*/ constructor(anchor){
super(), /**
* @type {number}
* @private
*/ this._size = 0, /**
* @type {Vec4}
* @private
*/ this._anchor = new Vec4(1, 1, 1, 1), /**
* @type {Color}
* @private
*/ this._colorX = COLOR_RED.clone(), /**
* @type {Color}
* @private
*/ this._colorY = COLOR_GREEN.clone(), /**
* @type {Color}
* @private
*/ this._colorZ = COLOR_BLUE.clone(), /**
* @type {Color}
* @private
*/ this._colorNeg = new Color(0.3, 0.3, 0.3), /**
* @type {number}
* @private
*/ this._radius = 10, /**
* @type {number}
* @private
*/ this._textSize = 10, /**
* @type {number}
* @private
*/ this._lineThickness = 2, /**
* @type {number}
* @private
*/ this._lineLength = 40;
// container
this.dom = document.createElement('div');
this.dom.id = 'view-cube-container';
this.dom.style.cssText = [
'position: absolute',
'margin: auto',
'pointer-events: none'
].join(';');
document.body.appendChild(this.dom);
this.anchor = anchor ?? this._anchor;
// construct svg root and group
this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this._svg.id = 'view-cube-svg';
this._group = document.createElementNS(this._svg.namespaceURI, 'g');
this._svg.appendChild(this._group);
// size
this._resize();
const colX = this._colorX.toString(false);
const colY = this._colorY.toString(false);
const colZ = this._colorZ.toString(false);
this._shapes = {
nx: this._circle(colX),
ny: this._circle(colY),
nz: this._circle(colZ),
px: this._circle(colX, true, 'X'),
py: this._circle(colY, true, 'Y'),
pz: this._circle(colZ, true, 'Z'),
xaxis: this._line(colX),
yaxis: this._line(colY),
zaxis: this._line(colZ)
};
this._shapes.px.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.RIGHT);
});
this._shapes.py.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.UP);
});
this._shapes.pz.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.BACK);
});
this._shapes.nx.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.LEFT);
});
this._shapes.ny.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.DOWN);
});
this._shapes.nz.children[0].addEventListener('pointerdown', ()=>{
this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.FORWARD);
});
this.dom.appendChild(this._svg);
}
}
/**
* Fired when the user clicks on a face of the view cube.
*
* @event
* @example
* const viewCube = new ViewCube()
* viewCube.on(ViewCube.EVENT_CAMERAALIGN, function (face) {
* console.log('Camera aligned to face: ' + face);
* });
*/ ViewCube.EVENT_CAMERAALIGN = 'camera:align';
export { ViewCube };