import { Utils } from '../common/Utils'
import { BasePoints } from './BasePoints';
import { PCDLoader } from '../loaders/PCDLoader';
import { PLYLoader } from '../loaders/PLYLoader';
const visibleFlag = 2;
/**
* Points - 点云
*
* 场景中的点物体,可展示点云数据,支持从文件加载(url)或直接传入坐标数组。
*
* @class Points
* @memberof THING
* @extends THING.BasePoints
* @public
*
* @example
* const points = new THING.Points({
* points: [[0, 0, 0], [1, 1, 0], [2, 0, 0]],
* size: 10,
* style: { color: '#ffffff' },
* });
*/
class Points extends BasePoints {
/**
* 场景中的点物体(可展示点云)
* @description 场景中的点物体(可展示点云),构造参数的优先级,url > points > selfPoints
* @param {object} param 初始化参数
* @param {string} [param.url] 点云文件路径
* @param {number[][]} [param.points] 点的坐标数组(世界坐标系).
* @param {number[][]} [param.selfPoints] 点的坐标数组(自身坐标系).
* @param {boolean} [param.autoAdjustPosition=false] 是否自动校正坐标
* @param {number[][]} [param.colors] 点的颜色数组.
* @param {number} [param.imageRatio=1] 贴图比例(如果点用贴图渲染的话)
* @param {number} [param.size=1] 单个点尺寸
* @param {number} [param.sizeAttenuation=false] 尺寸是否随着摄像机远近而变化
* @param {number[]} [param.pickedIds] 每个点的pickID的数组 长度与points长度一致
* @param {object} [param.styles] 点样式
* @example
const points = [
[0, 1, 0],
[1, 1, 0],
[2, 1, 0],
[3, 1, 0],
[0, 1, 4],
[1, 1, 4],
[2, 1, 4],
[3, 1, 4],
[0, 1, 0],
[0, 1, 1],
[0, 1, 2],
[0, 1, 3],
[4, 1, 0],
[4, 1, 1],
[4, 1, 2],
[4, 1, 3]
];
const pointsObj = new THING.Points({
points: points,
size: 3,
style: {
color: "#ff9900",
},
});
* @constructor
* @public
*/
constructor(param = {}) {
super(param);
}
// #region Private Functions
_adjustPosition(points) {
if (points instanceof Float32Array) {
let center = [0, 0, 0];
for (let i = 0; i < points.length; i += 3) {
center[0] += points[i];
center[1] += points[i + 1];
center[2] += points[i + 2];
}
center[0] /= points.length / 3;
center[1] /= points.length / 3;
center[2] /= points.length / 3;
this.position = center;
}
else {
super._adjustPosition(points);
}
}
_getResultByKeyType(key, array) {
if (key === 'Float32Array') {
return new Float32Array(array);
}
else {
const result = [];
for (let i = 0; i < array.length; i += this._colorStride) {
const color = [];
for (let j = 0; j < this._colorStride; j++) {
color[j] = array[i + j];
}
result.push(color);
}
return result;
}
}
_loadPoints(url, resolve, reject) {
const ext = url._getExtension();
let loader = null;
if (ext === 'pcd') {
loader = new PCDLoader();
}
else if (ext === 'ply') {
loader = new PLYLoader();
}
else {
console.error('Url is not pcd or ply');
reject();
return;
}
loader.load(url,
(data) => {
resolve({ positions: data.positions, colors: data.colors });
},
() => { },
(error) => {
reject(error);
}
)
}
_setPoints(value) {
let selfPoints = []
if (value instanceof Float32Array) {
this._originalPointType = 'Float32Array';
if (this._autoAdjustPosition) {
this._adjustPosition(value);
}
for (let i = 0; i < value.length; i += 3) {
const selfPoint = this.worldToSelf([value[i], value[i + 1], value[i + 2]])
selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);
}
}
else {
this._originalPointType = 'Array';
if (this._autoAdjustPosition) {
this._adjustPosition(value);
}
selfPoints = value.map(point => {
return this.worldToSelf(point);
}).flat(2);
}
this.bodyNode.setPoints(selfPoints);
this.bodyNode.setStates([]);
this.bodyNode.setColors([]);
this.onRefreshPoints();
}
_setSelfPoints(value) {
this._originalPointType = value instanceof Float32Array ? 'Float32Array' : 'Array';
const selfPoints = this._originalPointType === 'Float32Array' ? Array.from(value) : value.flat(2);
this.bodyNode.setPoints(selfPoints);
this.onRefreshPoints();
}
/**
* 初始化颜色数组(2维)的步长
* @param {Array<Array<number>>} colors 颜色数组。
*/
_initColorsStride(colors) {
if (!colors || colors.length === 0) {
return;
}
this._colorStride = colors[0].length === 4 ? 4 : 3;
}
/**
* 初始化颜色数组的类型
* @param {Array<number> | Array<Array<number>>} colors 颜色数组。
*/
_initColorsType(colors) {
if (!colors || colors.length === 0) {
return;
}
this._originalColorType = colors instanceof Float32Array ? 'Float32Array' : 'Array';
}
/**
* 根据点位数组(1维数组)以及颜色数组(1维数组)初始化颜色步长
* @param {Array<number>} points 点位数组。
* @param {Array<number>} colors 颜色数组。
*/
_initColorsStrideByFlatArray(points, colors) {
if (points.length === colors.length) {
this._colorStride = 3;
return;
}
this._colorStride = 4;
}
_parseColor(color) {
if (!color) {
return this._colorStride === 4 ? [1, 1, 1, 1] : [1, 1, 1];
}
if (color.length === this._colorStride) {
return color;
}
if (this._colorStride === 4) {
return [color[0], color[1], color[2], 1];
}
return [color[0], color[1], color[2]];
}
// #endregion
// #region Overrides
onCreateBodyNode(param) {
const node = Utils.createObject('Points', { app: this.app });
return node;
}
hasResource() {
return true;
}
onSetupResource(param) {
this.onSetupURL(param);
this._originalColorType = 'Array';
if (param['colors'] instanceof Float32Array) {
this._originalColorType = 'Float32Array';
}
this._size = Utils.parseNumber(param['size'], 1);
this._imageRatio = Utils.parseNumber(param['imageRatio'], 1);
this._pickedIds = Utils.parseValue(param['pickedIds'], []);
this._sizeAttenuation = false;
super.onSetupResource(param);
}
onSetupPoints() {
}
onLoadRenderableResource(param, resolve, reject) {
if (this.resource.url) {
this._loadPoints(this.resource.url, (data) => {
const bodyNode = Utils.createObject('Points', { app: this.app });
this.body.setNode(bodyNode);
if (this._autoAdjustPosition) {
this._adjustPosition(data.positions);
}
this._originalPointType = 'Float32Array'
this._originalColorType = 'Float32Array'
this._initColorsStrideByFlatArray(data.positions, data.colors);
this.bodyNode.begin()
{
this.bodyNode.setPoints(data.positions);
this.bodyNode.setColors(data.colors);
this.onRefreshPoints();
}
this.bodyNode.end()
if (this._pickedIds) {
this.bodyNode.setPickedIds(this._pickedIds);
}
if (this._imageRatio) {
this.bodyNode.setImageRatio(this._imageRatio);
}
resolve && resolve();
}, reject)
}
else {
const bodyNode = Utils.createObject('Points', { app: this.app });
this.body.setNode(bodyNode);
bodyNode.begin();
if (this.options['points']) {
this._setPoints(this.options['points']);
}
else if (this.options['selfPoints']) {
this._setSelfPoints(this.options['selfPoints']);
}
if (this.options['colors']) {
this.colors = this.options['colors'];
}
if (this.options['pickedIds']) {
this.pickedIds = this.options['pickedIds'];
}
if (this.options['imageRatio']) {
this.imageRatio = this.options['imageRatio'];
}
bodyNode.end();
resolve && resolve();
}
}
onSetupPosition(param) {
if (param['points']) {
this._adjustPosition(param['points'])
}
else {
super.onSetupPosition(param);
}
}
onSetupStyle(param) {
let style = param['style'];
if (style) {
// Enable transparent mode as default
if (style.transparent === undefined) {
style.transparent = true;
}
}
else {
param['style'] = {
transparent: true
}
}
super.onSetupStyle(param);
}
onRefreshPoints() {
this.bodyNode.setSize(this._size);
this.bodyNode.setSizeAttenuation(this._sizeAttenuation);
this.onBeforeSetBodyPoints();
this.onAfterSetBodyPoints();
}
onExportExternalData() {
let external = Object.assign({}, super.onExportExternalData());
if (!this._url) {
Utils.setAttributeIfExist(external, 'size', this);
Utils.setAttributeIfExist(external, 'imageRatio', this);
Utils.setAttributeIfExist(external, 'pickedIds', this);
Utils.setAttributeIfExist(external, 'colors', this);
}
return external;
}
clearPoints() {
const bodyNode = this.bodyNode;
bodyNode.begin();
bodyNode.setColors([]);
bodyNode.setPoints([]);
bodyNode.setStates([]);
bodyNode.end();
}
removePoint(index) {
const bodyNode = this.bodyNode;
bodyNode.begin();
const colors = bodyNode.getColors();
if (colors.length > 0) {
colors.splice(index * this._colorStride, this._colorStride);
}
const selfPoints = bodyNode.getPoints();
selfPoints.splice(index * 3, 3);
const states = bodyNode.getStates();
if (states.length > 0) {
states.splice(index, 1);
}
this.onRefreshPoints();
bodyNode.end();
}
insertPoint(index, position, color) {
const bodyNode = this.bodyNode;
bodyNode.begin();
const colors = bodyNode.getColors();
if (colors.length > 0) {
if (!color) {
color = Utils.getSimilarColor(colors, index, this._colorStride);
}
colors.splice(index * this._colorStride, 0, ...color);
}
const selfPoints = bodyNode.getPoints();
const selfPoint = this.worldToSelf(position)
selfPoints.splice(index * 3, 0, selfPoint[0], selfPoint[1], selfPoint[2]);
const states = bodyNode.getStates();
if (states.length > 0) {
states.splice(index, 0, visibleFlag);
}
this.onRefreshPoints();
bodyNode.end();
}
addPoint(position, color) {
const bodyNode = this.bodyNode;
bodyNode.begin();
const colors = bodyNode.getColors();
if (colors.length > 0) {
if (!color) {
color = Utils.getSimilarColor(colors, null, this._colorStride);
}
colors.push(...color);
}
const selfPoints = bodyNode.getPoints();
const selfPoint = this.worldToSelf(position)
selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);
const states = bodyNode.getStates();
if (states.length > 0) {
states.push(visibleFlag);
}
this.onRefreshPoints();
bodyNode.end();
}
addPoints(points, colors) {
const bodyNode = this.bodyNode;
bodyNode.begin();
const bodyColors = bodyNode.getColors();
if (bodyColors.length > 0) {
if (colors) {
colors.forEach(color => {
color = this._parseColor(color);
bodyColors.push(...color);
})
}
else {
const color = Utils.getSimilarColor(bodyColors, null, this._colorStride);
for (let i = 0; i < points.length; i++) {
bodyColors.push(...color);
}
}
}
const selfPoints = bodyNode.getPoints();
const states = bodyNode.getStates();
points.forEach(point => {
const selfPoint = this.worldToSelf(point)
selfPoints.push(selfPoint[0], selfPoint[1], selfPoint[2]);
if (states.length > 0) {
states.push(visibleFlag);
}
});
this.onRefreshPoints();
bodyNode.end();
}
setPoint(index, position, color) {
const bodyNode = this.bodyNode;
bodyNode.begin();
const colors = bodyNode.getColors();
if (colors.length > 0) {
if (color) {
color = this._parseColor(color);
}
else {
color = Utils.getSimilarColor(colors, index, this._colorStride);
}
for (let i = 0; i < color; i++) {
colors[index * this._colorStride + i] = color[i];
}
}
const selfPoints = bodyNode.getPoints();
const selfPoint = this.worldToSelf(position)
selfPoints[index * 3] = selfPoint[0];
selfPoints[index * 3 + 1] = selfPoint[1];
selfPoints[index * 3 + 2] = selfPoint[2];
this.onRefreshPoints();
bodyNode.end();
}
// #endregion
// #region Common
/**
* Set the visibility of the specified index point.
* @param {number|Array} index 索引。
* @param {boolean} visible 可见性。
*/
setVisibleByIndex(index, visible) {
const bodyNode = this.bodyNode;
const states = bodyNode.getStates();
if (states.length === 0) {
const points = bodyNode.getPoints();
bodyNode.setStates(new Array(points.length / 3).fill(visibleFlag));
}
if (Utils.isArray(index)) {
bodyNode.begin();
index.forEach(i => {
bodyNode.setVisibleByIndex(i, visible);
})
bodyNode.end();
}
else {
bodyNode.setVisibleByIndex(index, visible);
}
}
/**
* Get the visibility of the specified index point.
* @param {number|Array} index 索引。
* @returns {Array<boolean>} 返回可见性。
*/
getVisibleByIndex(index) {
const bodyNode = this.bodyNode;
if (Utils.isArray(index)) {
let visibles = [];
index.forEach(i => {
visibles.push(bodyNode.getVisibleByIndex(i));
})
return visibles;
}
else {
return bodyNode.getVisibleByIndex(index);
}
}
/**
* Sets the color of the specified index point.
* @param {number|Array} index 索引。
* @param {Array<number>} color 颜色。
*/
setColorByIndex(index, color) {
color = this._parseColor(color);
const bodyNode = this.bodyNode;
if (Utils.isArray(index)) {
bodyNode.begin();
index.forEach(i => {
bodyNode.setColorByIndex(i, color);
})
bodyNode.end();
}
else {
bodyNode.setColorByIndex(index, color);
}
}
/**
* Get the color of the specified index point.
* @param {number|Array} index 索引。
* @returns {Array<number>} 返回颜色。
*/
getColorByIndex(index) {
const bodyNode = this.bodyNode;
if (Utils.isArray(index)) {
let colors = [];
index.forEach(i => {
colors.push(bodyNode.getColorByIndex(i));
})
return colors;
}
else {
return bodyNode.getColorByIndex(index);
}
}
// #endregion
// #region Accessor
/**
* 获取/设置点大小
* @type {number}
* @default 1.0
* @public
*/
get size() {
return this._size;
}
set size(value) {
this._size = value;
this.bodyNode.setSize(value);
}
/**
* 获取/设置贴图比例
* @type {number}
* @default 1.0
* @public
*/
get imageRatio() {
return this._imageRatio;
}
set imageRatio(value) {
this._imageRatio = value;
this.bodyNode.setImageRatio(value);
}
/**
* 开启/关闭尺寸是否随着摄像机远近而变化
* @default false
* @type {boolean}
* @public
*/
get sizeAttenuation() {
return this._sizeAttenuation;
}
set sizeAttenuation(value) {
this._sizeAttenuation = value;
this.bodyNode.setSizeAttenuation(value);
}
/**
* 获取/设置 pickId的数组
* @type {Array<number>}
* @public
*/
get pickedIds() {
return this.bodyNode.getPickedIds().slice(0);
}
set pickedIds(value) {
this.bodyNode.setPickedIds(value);
}
/**
* 获取/设置colors数组
* @type {Array<Array<number>> | Float32Array<number>}
* @public
*/
get colors() {
const colors = this.bodyNode.getColors();
return this._getResultByKeyType(this._originalColorType, colors)
}
set colors(value) {
// 初始化颜色数组类型
this._initColorsType(value);
// 根据数组类型设置颜色
if (this._originalColorType === 'Float32Array') {
// float32数组需要根据点位数组和颜色数组一起判断rgb还是rgba
this._initColorsStrideByFlatArray(this.bodyNode.getPoints(), value);
this.bodyNode.setColors(Array.from(value), this._colorStride === 4);
}
else {
this._initColorsStride(value);
this.bodyNode.setColors(value.flat(2), this._colorStride === 4);
}
}
/**
* Get/Set Url
* @type {string}
* @public
*/
get url() {
return this.resource.url;
}
set url(value) {
const resource = THING.Utils.cloneObject(this.resource)
resource.url = value;
this.resource = resource;
}
get selfPoints() {
const selfPoints = this.bodyNode.getPoints();
return this._getResultByKeyType(this._originalPointType, selfPoints)
}
set selfPoints(value) {
if (value) {
this.bodyNode.begin();
this._setSelfPoints(value);
this.bodyNode.end();
}
else {
this.clearPoints();
}
}
/**
* 获取/设置顶点数组
* 注意,每次设置Points之后会清空颜色和显隐状态,需要先设置points,在设置colors和visible
* @type {Array<Array<number>>}
* @public
*/
get points() {
let points = this.bodyNode.getPoints();
if (this._originalPointType === 'Float32Array') {
const tempPoints = [];
for (let i = 0; i < points.length; i += 3) {
const point = this.selfToWorld([points[i], points[i + 1], points[i + 2]]);
tempPoints.push(point[0], point[1], point[2]);
}
return new Float32Array(tempPoints);
}
else {
const result = [];
for (let i = 0; i < points.length; i += 3) {
result.push(this.selfToWorld([points[i], points[i + 1], points[i + 2]]));
}
return result;
}
}
set points(value) {
if (value) {
this.bodyNode.begin();
this._setPoints(value);
this.bodyNode.end();
}
else {
this.clearPoints();
}
}
get pointsLength() {
const points = this.bodyNode.getPoints();
return points.length / 3;
}
// #endregion
get isPoints() {
return true;
}
}
export { Points }