xeogl / API Docs

API Docs for: 1.0.0

File: examples/js/annotations/pin.js

```/**
A **Pin** is a pinned position on the surface of an {{#crossLink "Entity"}}{{/crossLink}}.

## Overview

#### Position

An Pin is positioned within one of the triangles of its {{#crossLink "Entity"}}Entity's{{/crossLink}} {{#crossLink "Geometry"}}{{/crossLink}}. Wherever that triangles goes within the 3D view, the Pin will automatically follow. An Pin specifies its position with two properties:

* {{#crossLink "Pin/bary:property"}}{{/crossLink}}, the barycentric coordinates of the position within the triangle.

From these, an Pin dynamically calculates its Cartesian coordinates, which it provides in each xeogl coordinate space:

#### Visibility

* {{#crossLink "Pin/occludable:property"}}{{/crossLink}} specifies whether the Pin becomes invisible whenever its occluded by other objects in the 3D view, and

* To determine if each Pin is occluded, xeogl renders the scene to a hidden framebuffer, along with a colored point for each Pin's position. Then xeogl determines each Pin to be occluded if the pixel at its position does not match the special pin color. The color is configured as some unusual color that is not used elsewhere in the scene.

## Example

In the example below we'll create an {{#crossLink "Entity"}}{{/crossLink}} with a {{#crossLink "TorusGeometry"}}{{/crossLink}}, then attach a Pin to it. Then we'll subscribe to changes to the Pin's position and visibility status.
````javascript
var entity = new xeogl.Entity({
geometry: new xeogl.TorusGeometry(),
transform: new xeogl.Translate({
xyz: [0,0,0]
});
});

var pin = new xeogl.Pin({
entity: entity,
primIndex: 12,              // Triangle index in Geometry indices array
bary: [0.11, 0.79, 0.08]    // Barycentric coordinates in the triangle
});

var localPos   = pin.localPos;     // 3D Local-space position
var worldPos   = pin.worldPos;     // 3D World-space position
var viewPos    = pin.viewPos;      // 3D View-space position
var canvasPos  = pin.canvasPos;    // 2D Canvas-space position

// Listen for change of the Pin's Local-space cartesian position,
// which is caused by modification of Pin's Geometry or update to
// the Pin's 'primIndex' or 'bary' properties.
pin.on("localPos", function() {
//...
});

// Listen for change of the Pin's World-space cartesian position,
// which is caused by update of Pin's Local-space position or by
// animation of the Entity by its modelling Transform.
pin.on("worldPos", function() {
//...
});

// Listen for change of Pin visibility. The Pin becomes invisible when it
// falls outside the canvas, or when an object occludes the Pin's position
// in the 3D view.
pin.on("visible", function(visible) { // Pin visibility has changed
if (visible) {
this.log("Pin is not occluded and within canvas.");
} else {
this.log("Pin is either occluded or is off the canvas.");
}
});
````
@class Pin
@module xeogl
@submodule annotations
@constructor
@param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this Pin in the default
@param [cfg] {*} Configs
@param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
generated automatically when omitted.
@param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Pin.
@param [cfg.entity] {Number|String|Entity} ID or instance of the {{#crossLink "Entity"}}{{/crossLink}} the Pin is attached to.
@param [cfg.primIndex=0] {Number} Index of the triangle containing the Pin. Within the {{#crossLink "Entity"}}Entity's{{/crossLink}} {{#crossLink "Geometry/indices:property"}}Geometry indices{{/crossLink}}, this is the index of the first vertex for the triangle.
@param [cfg.bary=[0.3,0.3,0.3]] {Float32Array} Barycentric coordinates of the Pin within its triangle.
@param [cfg.offset=0.2] {Number} How far the Pin is lifted out of its triangle, along the surface normal vector. This is used when occlusion culling, to ensure that the Pin is not lost inside the surface it's attached to.
@param [cfg.occludable=false] {Boolean} Indicates whether occlusion testing is performed for the Pin, where it will be flagged invisible whenever it's hidden by something else in the 3D view.
@extends Component
*/
(function () {

"use strict";

const PIN_COLOR = xeogl.math.vec3([1.0, 1.0, 0.0]);

// Do occlusion test per this number of ticks. Having a high-ish number
// gives a nice hysteresis where, when occluded, the label remains visible
// for a moment before disappearing, which feels smooth.
const TEST_TICKS = 20;

// Each Scene gets a VisibilityTester, which periodically tests if registered
// pins are visible, which is when they are within the canvas boundary and
// are not obscured by any objects in the 3D view.

var VisibilityTester = function (scene) {

this._scene = scene;
this._pins = {};
this._markers = {};
this._testablePins = {};

var countDown = TEST_TICKS;

scene.on("tick", function () {
if (--countDown <= 0) {
this._runTest();
countDown = TEST_TICKS;
}
}, this);
};

VisibilityTester.prototype._runTest = (function () {

var testList = [];
var pixels = [];
var colors = [];

return function () {

var canvas = this._scene.canvas;
var pin;
var canvasPos;
var canvasX;
var canvasY;
var lenPixels = 0;
var i;
var boundary = canvas.boundary;
var canvasWidth = boundary[2];
var canvasHeight = boundary[2];
var testListLen = 0;

// Hide pins that fall outside canvas

for (var id in this._testablePins) {
if (this._testablePins.hasOwnProperty(id)) {

pin = this._testablePins[id];

canvasPos = pin.canvasPos;

canvasX = canvasPos[0];
canvasY = canvasPos[1];

if ((canvasX + 10) < 0 ||
(canvasY + 10) < 0 ||
(canvasX - 10) > canvasWidth ||
(canvasY - 10) > canvasHeight) {

pin._setVisible(false);

} else if (pin.occludable) {

testList[testListLen++] = pin;

pixels[lenPixels++] = canvasX;
pixels[lenPixels++] = canvasY;

} else {
pin._setVisible(true);
}
}
}

// Hide pins that are occluded by 3D objects

var opaqueOnly = true;

var r = PIN_COLOR[0] * 255;
var g = PIN_COLOR[1] * 255;
var b = PIN_COLOR[2] * 255;

for (i = 0; i < testListLen; i++) {
pin = testList[i];
pin._setVisible((colors[i * 4] === r) && (colors[i * 4 + 1] === g) && (colors[i * 4 + 2] === b));
}
};
})();

// Registers a Pin for visibility testing
var pinId = pin.id;
this._pins[pinId] = pin;

// Entity which renders a 3D point that we'll test for occlusion
this._markers[pinId] = new xeogl.Entity(this._scene, {
lights: this._scene.components["_pinMarkerLights"] || new xeogl.Lights(this._scene, {
id: "_pinMarkerLights",
lights: []
}),
geometry: this._scene.components["_pinMarkerGeometry"] || new xeogl.Geometry(this._scene, {
id: "_pinMarkerGeometry",
primitive: "points",
positions: [0, 0, 0],
indices: [0]
}),
material: this._scene.components["_pinMarkerMaterial"] || new xeogl.PhongMaterial(this._scene, {
id: "_pinMarkerMaterial",
emissive: PIN_COLOR,
diffuse: [0, 0, 0],
specular: [0, 0, 0],
pointSize: 5
}),
transform: {type: "xeogl.Translate"},
visible: true
});
this._testablePins[pinId] = pin;
};

// Enables or disables occlusion testing for a Pin
VisibilityTester.prototype.setPinTestable = function (pinId, testable) {
var pin = this._pins[pinId];
if (pin) {
this._markers[pinId].visible = testable;
testable ? this._testablePins[pinId] = pin : delete this._testablePins[pinId];
}
};

// Updates the World-space position of a Pin
VisibilityTester.prototype.setPinWorldPos = function (pinId, worldPos) {
this._markers[pinId].transform.xyz = worldPos;
};

// De-registers a Pin, so that it is not tested for visibility
VisibilityTester.prototype.removePin = function (pinId) {
var info = this._pins[pinId];
if (info) {
delete this._pins[pinId];
this._markers[pinId].destroy();
delete this._markers[pinId];
delete this._testablePins[pinId];
}
};

var visibilityTesters = {};

function getVisibilityTester(scene) {
var visibilityTester = visibilityTesters[scene.id];
if (!visibilityTester) {
visibilityTester = new VisibilityTester(scene);
visibilityTesters[scene.id] = visibilityTester;
scene.on("destroyed", function () {
delete visibilityTesters[scene.id];
})
}
return visibilityTester;
}

xeogl.Pin = xeogl.Component.extend({

type: "xeogl.Pin",

_init: function (cfg) {

this._visible = null;
this._localPos = new Float32Array(3);
this._worldPos = new Float32Array(3);
this._viewPos = new Float32Array(3);
this._canvasPos = new Float32Array(2);
this._localNormal = new Float32Array(3);
this._worldNormal = new Float32Array(3);

this._localPosDirty = true;
this._worldPosDirty = false;

this.entity = cfg.entity;
this.primIndex = cfg.primIndex;
this.bary = cfg.bary;
this.offset = cfg.offset;
this.occludable = cfg.occludable;
},

_props: {

/**

You can attach a Pin to a different {{#crossLink "Entity"}}{{/crossLink}} at any time.

@property entity
@type {Number | String | xeogl.Entity}
*/
entity: {

set: function (value) {

/**
* @event entity
* @param value The property's new value
*/
this._attach({
name: "entity",
type: "xeogl.Entity",
component: value,
sceneDefault: false,
onAttached: {callback: this._entityAttached, scope: this},
onDetached: {callback: this._entityDetached, scope: this}
});
},

get: function () {
return this._attached.entity;
}
},

/**
Index of the triangle containing this pin.

@property primIndex
@default 0
@type Number
*/
primIndex: {

set: function (value) {

value = value || 0;

if (value === this._primIndex) {
return;
}

this._primIndex = value;

this._setLocalPosDirty();

/**
*
* @event primIndex
* @param value Number The property's new value
*/
this.fire("primIndex", this._primIndex);
},

get: function () {
return this._primIndex;
}
},

/**
Barycentric coordinates of this Pin within its triangle.

A value of ````[0.3, 0.3, 0.3]```` is the center of the triangle.

@property bary
@default [0.3,0.3,0.3]
@type Float32Array
*/
bary: {

set: function (value) {
this._bary = value || xeogl.math.vec3([.3, .3, .3]);
this._setLocalPosDirty();

/**
* @event bary
* @param value Float32Array The property's new value
*/
this.fire("bary", this._bary);
},

get: function () {
return this._bary;
}
},

/**
How far the Pin is lifted out of its triangle, along the surface normal vector.

This is used when occlusion culling, to ensure that the Pin is not lost inside
the surface it's attached to.

@property offset
@default 0.2
@type Number
*/
offset: {

set: function (value) {
this._offset = value !== undefined ? value : 0.2;
this._setWorldPosDirty();

/**
*
* @event offset
* @param value Number The property's new value
*/
this.fire("offset", this._offset);
},

get: function () {
return this._offset;
}
},

/**
Indicates whether occlusion testing is performed for this Pin.

be false whenever the Pin is hidden behind something else in the 3D view.

Set this false if the Pin is to remain visible when hidden behind things while
being within the canvas.

@property occludable
@default false
@type Float32Array
*/
occludable: {

set: function (value) {

value = !!value;

if (value === this._occludable) {
return;
}

this._occludable = value;

if (this._occludable) {
if (!this._visTester) {
this._visTester = getVisibilityTester(this.scene);
}
} else {
if (this._visTester) {
this._visTester.removePin(this);
}
this._setVisible(true);
}

/**
* @event occludable
* @param value Float32Array The property's new value
*/
this.fire("occludable", this._occludable);
},

get: function () {
return this._occludable;
}
},

/**
Local-space 3D coordinates of this Pin.

This is read-only and is automatically calculated.

@property localPos
@default [0,0,0]
@type Float32Array
@final
*/
localPos: {

get: function () {
this.__update();
return this._localPos;
}
},

/**
World-space 3D coordinates of this Pin.

This is read-only and is automatically calculated.

@property worldPos
@default [0,0,0]
@type Float32Array
@final
*/
worldPos: {

get: function () {
this.__update();
return this._worldPos;
}
},

/**
View-space 3D coordinates of this Pin.

This is read-only and is automatically calculated.

@property viewPos
@default [0,0]
@type Float32Array
@final
*/
viewPos: {

get: function () {
this.__update();
xeogl.math.transformPoint3(this.scene.camera.view.matrix, this.worldPos, this._viewPos);
return this._viewPos;
}
},

/**
Canvas-space 2D coordinates of this Pin.

This is read-only and is automatically calculated.

@property canvasPos
@default [0,0]
@type Float32Array
@final
*/
canvasPos: {

get: (function () {

var tempVec4a = xeogl.math.vec4([0, 0, 0, 1]);
var tempVec4b = xeogl.math.vec4();

return function () {

tempVec4a.set(this.viewPos);

xeogl.math.transformPoint4(this.scene.camera.project.matrix, tempVec4a, tempVec4b);

var aabb = this.scene.canvas.boundary;

this._canvasPos[0] = Math.floor((1 + tempVec4b[0] / tempVec4b[3]) * aabb[2] / 2);
this._canvasPos[1] = Math.floor((1 - tempVec4b[1] / tempVec4b[3]) * aabb[3] / 2);

return this._canvasPos;
};
})()
},

/**
World-space normal vector of this Pin.

This is read-only and is automatically calculated.

@property worldNormal
@default [0,0,1]
@type Float32Array
@final
*/
worldNormal: {

get: function () {
this.__update();
return this._worldNormal;
}
},

/**
Indicates if this Pin is currently visible.

This is read-only and is automatically calculated.

The Pin is invisible whenever:

* {{#crossLink "Pin/occludable:property"}}{{/crossLink}} is true and the Pin is currently occluded by something in the 3D view.

@property visible
@default true
@type Boolean
@final
*/
visible: {

get: function () {
return !!this._visible;
}
}
},

_entityAttached: function (entity) {
this._onEntityLocalBoundary = entity.localBoundary.on("updated", this._setLocalPosDirty, this);
this._onEntityWorldBoundary = entity.worldBoundary.on("updated", this._setWorldPosDirty, this);
this._onEntityVisible = entity.on("visible", this._entityVisible, this);
this._setLocalPosDirty();
},

_setLocalPosDirty: function () {
if (!this._localPosDirty) {
this._localPosDirty = true;
this._needUpdate();
}
},

_setWorldPosDirty: function () {
if (!this._worldPosDirty) {
this._worldPosDirty = true;
this._needUpdate();
}
},

_entityVisible: function (visible) {
if (!visible) {
this._setVisible(false);
}
if (this._visTester) {
this._visTester.setPinTestable(this.id, visible);
}
},

_entityDetached: function (entity) {
entity.localBoundary.off(this._onEntityLocalBoundary);
entity.worldBoundary.off(this._onEntityWorldBoundary);
entity.off(this._onEntityVisible);
this._entityVisible(false);
},

_update: function () {

var localPosDirty = this._localPosDirty;
var localNormalDirty = this._localNormalDirty;
var worldPosDirty = localPosDirty || this._worldPosDirty;

this.__update();

if (localPosDirty) {

/**
* @event localPos
*/
this.fire("localPos", this._localPos);
}

if (worldPosDirty) {

/**
* @event worldPos
*/
this.fire("worldPos", this._worldPos);

/**
* @event worldNormal
*/
this.fire("worldNormal", this._worldNormal);
}
},

__update: (function () {

var math = xeogl.math;
var a = math.vec3();
var b = math.vec3();
var c = math.vec3();
var normal = math.vec3();

return function () {

var entity = this._attached.entity;

if (!entity) {
return;
}

if (this.destroyed) {
return;
}

if (this._localPosDirty) {

// Get Local position from entity's Geometry, primitive index and barycentric coordinates

var geometry = entity.geometry;
var indices = geometry.indices;
var positions = geometry.positions;

if (!indices || !positions) {
return;
}

var i = this._primIndex;

var ia = indices[i];
var ib = indices[i + 1];
var ic = indices[i + 2];

var ia3 = ia * 3;
var ib3 = ib * 3;
var ic3 = ic * 3;

a[0] = positions[ia3];
a[1] = positions[ia3 + 1];
a[2] = positions[ia3 + 2];

b[0] = positions[ib3];
b[1] = positions[ib3 + 1];
b[2] = positions[ib3 + 2];

c[0] = positions[ic3];
c[1] = positions[ic3 + 1];
c[2] = positions[ic3 + 2];

math.barycentricToCartesian(this._bary, a, b, c, this._localPos);

// Lift the cartesian coords out of the plane of the triangle
math.triangleNormal(a, b, c, normal);
math.mulVec3Scalar(normal, this._offset, normal);

this._localPosDirty = false;
this._worldPosDirty = true;
this._worldNormalDirty = true;
}

if (this._worldPosDirty) {

// Transform Local position into World space

var transform = entity.transform;
transform ? math.transformPoint3(transform.leafMatrix, this._localPos, this._worldPos) : this._worldPos.set(this._localPos);

if (this._visTester) {
this._visTester.setPinWorldPos(this.id, this._worldPos);
}

this._worldPosDirty = false;
}

if (this._worldNormalDirty) {

// Transform Local normal into World space

var transform = entity.transform;
transform ? math.transformVec3(transform.leafMatrix, this._localNormal, this._worldNormal) : this._worldNormal.set(this._localNormal);

this._worldNormalDirty = false;
}
};

})(),

_setVisible: function (value) { // Called by VisibilityTester

if (this._visible === value) {
return;
}

this._visible = value !== false;

/**
* @event visible
* @param value Float32Array The property's new value
*/
this.fire("visible", this._visible);
},

_getJSON: function () {
var json = {
primIndex: this._primIndex,
bary: xeogl.math.vecToArray(this._bary),
offset: this._offset,
occludable: this._occludable
};
if (this._attached.entity) {
json.entity = this._attached.entity.id;
}
return json;
},

_destroy: function () {
if (this._visTester) {
this._visTester.removePin(this.id);
}
}
});
})();

```