Skip to content
章节导航

path.module.js 使用文档

path是three.js的扩展,可以方便地创建3D几何路径。

使用方法

步骤1: 创建 PathPointList 以预处理并存储点。

js
import { PathPointList, PathGeometry } from "./path.module";
const list = new PathPointList();
/**
 * 设置点
 * @param {THREE.Vector3[]} points 关键点数组
 * @param {number} cornerRadius 角落半径,设为0以禁用圆角,默认为0.1
 * @param {number} cornerSplit 角落分割数,默认为10
 * @param {number} up 强制向上,默认通过切线自动计算
 * @param {boolean} close 是否关闭路径,默认为false
 */
list.set(points, 0.1, 10, undefined, false);

步骤2: 生成几何体

生成 PathGeometry

js
/**
 * @param {Object|Number} initData - 如果 initData 是数字,几何体通过空数据初始化并设置为最大顶点数。如果 initData 是对象,它包含 pathPointList 和选项。
 * @param {Boolean} [generateUv2=false] 是否生成第二组UV坐标
 */
// 使用最大顶点数初始化
const geometry = new THREE.PathGeometry(2000, false);
// 使用数据初始化
const geometry = new THREE.PathGeometry({
    pathPointList: pathPointList,
    options: {
        width: 0.1, // 默认为0.1
        arrow: true, // 默认为true
        progress: 1, // 默认为1
        side: "both" // "left"/"right"/"both",默认为"both"
    },
    usage: THREE.StaticDrawUsage // 几何体用途
}, false);

// 当 pathPointList 改变时更新几何体
geometry.update(pathPointList, {
    width: 0.1, // 默认为0.1
    arrow: true, // 默认为true
    progress: 1, // 默认为1
    side: "both" // "left"/"right"/"both",默认为"both"
});

或者生成 PathTubeGeometry

js
/**
 * @param {Object|Number} initData - 如果 initData 是数字,几何体通过空数据初始化并设置为最大顶点数。如果 initData 是对象,它包含 pathPointList 和选项。
 * @param {Boolean} [generateUv2=false] 是否生成第二组UV坐标
 */
// 使用最大顶点数初始化
const geometry = new THREE.PathTubeGeometry(2000, false);
// 使用数据初始化
const geometry = new THREE.PathTubeGeometry({
    pathPointList: pathPointList,
    options: {
        radius: 0.1, // 默认为0.1
        radialSegments: 8, // 默认为8
        progress: 1, // 默认为1
        startRad: 0 // 默认为0
    },
    usage: THREE.StaticDrawUsage // 几何体用途
}, false);

// 当 pathPointList 改变时更新几何体
geometry.update(pathPointList, {
    radius: 0.1, // 默认为0.1
    radialSegments: 8, // 默认为8
    progress: 1, // 默认为1
    startRad: 0 // 默认为0
});

源码

path.module.js

js
import { Vector3, Matrix4, QuadraticBezierCurve3, BufferGeometry, BufferAttribute, DynamicDrawUsage, Uint32BufferAttribute, Uint16BufferAttribute, StaticDrawUsage } from 'three';

/**
 * PathPoint
 */
class PathPoint {

	constructor() {
		this.pos = new Vector3();
		this.dir = new Vector3();
		this.right = new Vector3();
		this.up = new Vector3(); // normal
		this.dist = 0; // distance from start
		this.widthScale = 1; // for corner
		this.sharp = false; // marks as sharp corner
	}

	lerpPathPoints(p1, p2, alpha) {
		this.pos.lerpVectors(p1.pos, p2.pos, alpha);
		this.dir.lerpVectors(p1.dir, p2.dir, alpha);
		this.up.lerpVectors(p1.up, p2.up, alpha);
		this.right.lerpVectors(p1.right, p2.right, alpha);
		this.dist = (p2.dist - p1.dist) * alpha + p1.dist;
		this.widthScale = (p2.widthScale - p1.widthScale) * alpha + p1.widthScale;
	}

	copy(source) {
		this.pos.copy(source.pos);
		this.dir.copy(source.dir);
		this.up.copy(source.up);
		this.right.copy(source.right);
		this.dist = source.dist;
		this.widthScale = source.widthScale;
	}

}

/**
 * PathPointList
 * input points to generate a PathPoint list
 */
class PathPointList {

	constructor() {
		this.array = []; // path point array
		this.count = 0;
	}

	/**
	 * Set points
	 * @param {Vector3[]} points key points array
	 * @param {number} cornerRadius? the corner radius. set 0 to disable round corner. default is 0.1
	 * @param {number} cornerSplit? the corner split. default is 10.
	 * @param {number} up? force up. default is auto up (calculate by tangent).
	 * @param {boolean} close? close path. default is false.
	 */
	set(points, cornerRadius = 0.1, cornerSplit = 10, up = null, close = false) {
		points = points.slice(0);

		if (points.length < 2) {
			console.warn('PathPointList: points length less than 2.');
			this.count = 0;
			return;
		}

		// Auto close
		if (close && !points[0].equals(points[points.length - 1])) {
			points.push(new Vector3().copy(points[0]));
		}

		// Generate path point list
		for (let i = 0, l = points.length; i < l; i++) {
			if (i === 0) {
				this._start(points[i], points[i + 1], up);
			} else if (i === l - 1) {
				if (close) {
					// Connect end point and start point
					this._corner(points[i], points[1], cornerRadius, cornerSplit, up);

					// Fix start point
					const dist = this.array[0].dist; // should not copy dist
					this.array[0].copy(this.array[this.count - 1]);
					this.array[0].dist = dist;
				} else {
					this._end(points[i]);
				}
			} else {
				this._corner(points[i], points[i + 1], cornerRadius, cornerSplit, up);
			}
		}
	}

	/**
	 * Get distance of this path
	 * @return {number}
	 */
	distance() {
		if (this.count > 0) {
			return this.array[this.count - 1].dist;
		}
		return 0;
	}

	_getByIndex(index) {
		if (!this.array[index]) {
			this.array[index] = new PathPoint();
		}
		return this.array[index];
	}

	_start(current, next, up) {
		this.count = 0;

		const point = this._getByIndex(this.count);

		point.pos.copy(current);
		point.dir.subVectors(next, current);

		// init start up dir
		if (up) {
			point.up.copy(up);
		} else {
			// select an initial normal vector perpendicular to the first tangent vector
			let min = Number.MAX_VALUE;
			const tx = Math.abs(point.dir.x);
			const ty = Math.abs(point.dir.y);
			const tz = Math.abs(point.dir.z);
			if (tx < min) {
				min = tx;
				point.up.set(1, 0, 0);
			}
			if (ty < min) {
				min = ty;
				point.up.set(0, 1, 0);
			}
			if (tz < min) {
				point.up.set(0, 0, 1);
			}
		}

		point.right.crossVectors(point.dir, point.up).normalize();
		point.up.crossVectors(point.right, point.dir).normalize();
		point.dist = 0;
		point.widthScale = 1;
		point.sharp = false;

		point.dir.normalize();

		this.count++;
	}

	_end(current) {
		const lastPoint = this.array[this.count - 1];
		const point = this._getByIndex(this.count);

		point.pos.copy(current);
		point.dir.subVectors(current, lastPoint.pos);
		const dist = point.dir.length();
		point.dir.normalize();

		point.up.copy(lastPoint.up); // copy last up

		const vec = helpVec3_1.crossVectors(lastPoint.dir, point.dir);
		if (vec.length() > Number.EPSILON) {
			vec.normalize();
			const theta = Math.acos(Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)); // clamp for floating pt errors
			point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
		}

		point.right.crossVectors(point.dir, point.up).normalize();

		point.dist = lastPoint.dist + dist;
		point.widthScale = 1;
		point.sharp = false;

		this.count++;
	}

	_corner(current, next, cornerRadius, cornerSplit, up) {
		if (cornerRadius > 0 && cornerSplit > 0) {
			const lastPoint = this.array[this.count - 1];
			const curve = _getCornerBezierCurve(lastPoint.pos, current, next, cornerRadius, (this.count - 1) === 0, helpCurve);
			const samplerPoints = curve.getPoints(cornerSplit); // TODO optimize

			for (let f = 0; f < cornerSplit; f++) {
				this._sharpCorner(samplerPoints[f], samplerPoints[f + 1], up, f === 0 ? 1 : 0);
			}

			if (!samplerPoints[cornerSplit].equals(next)) {
				this._sharpCorner(samplerPoints[cornerSplit], next, up, 2);
			}
		} else {
			this._sharpCorner(current, next, up, 0, true);
		}
	}

	// dirType: 0 - use middle dir / 1 - use last dir / 2- use next dir
	_sharpCorner(current, next, up, dirType = 0, sharp = false) {
		const lastPoint = this.array[this.count - 1];
		const point = this._getByIndex(this.count);

		const lastDir = helpVec3_1.subVectors(current, lastPoint.pos);
		const nextDir = helpVec3_2.subVectors(next, current);

		const lastDirLength = lastDir.length();

		lastDir.normalize();
		nextDir.normalize();

		point.pos.copy(current);

		if (dirType === 1) {
			point.dir.copy(lastDir);
		} else if (dirType === 2) {
			point.dir.copy(nextDir);
		} else {
			point.dir.addVectors(lastDir, nextDir);
			point.dir.normalize();
		}

		if (up) {
			if (point.dir.dot(up) === 1) {
				point.right.crossVectors(nextDir, up).normalize();
			} else {
				point.right.crossVectors(point.dir, up).normalize();
			}

			point.up.crossVectors(point.right, point.dir).normalize();
		} else {
			point.up.copy(lastPoint.up);

			const vec = helpVec3_3.crossVectors(lastPoint.dir, point.dir);
			if (vec.length() > Number.EPSILON) {
				vec.normalize();
				const theta = Math.acos(Math.min(Math.max(lastPoint.dir.dot(point.dir), -1), 1)); // clamp for floating pt errors
				point.up.applyMatrix4(helpMat4.makeRotationAxis(vec, theta));
			}

			point.right.crossVectors(point.dir, point.up).normalize();
		}

		point.dist = lastPoint.dist + lastDirLength;

		const _cos = lastDir.dot(nextDir);
		point.widthScale = Math.min(1 / Math.sqrt((1 + _cos) / 2), 1.415) || 1;
		point.sharp = (Math.abs(_cos - 1) > 0.05) && sharp;

		this.count++;
	}

}

const helpVec3_1 = new Vector3();
const helpVec3_2 = new Vector3();
const helpVec3_3 = new Vector3();
const helpMat4 = new Matrix4();
const helpCurve = new QuadraticBezierCurve3();

function _getCornerBezierCurve(last, current, next, cornerRadius, firstCorner, out) {
	const lastDir = helpVec3_1.subVectors(current, last);
	const nextDir = helpVec3_2.subVectors(next, current);

	const lastDirLength = lastDir.length();
	const nextDirLength = nextDir.length();

	lastDir.normalize();
	nextDir.normalize();

	// cornerRadius can not bigger then lineDistance / 2, auto fix this
	const v0Dist = Math.min((firstCorner ? lastDirLength / 2 : lastDirLength) * 0.999999, cornerRadius);
	out.v0.copy(current).sub(lastDir.multiplyScalar(v0Dist));

	out.v1.copy(current);

	const v2Dist = Math.min(nextDirLength / 2 * 0.999999, cornerRadius);
	out.v2.copy(current).add(nextDir.multiplyScalar(v2Dist));

	return out;
}

/**
 * PathGeometry
 */
class PathGeometry extends BufferGeometry {

	/**
	 * @param {object|number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
	 * @param {boolean} [generateUv2=false]
	 */
	constructor(initData = 3000, generateUv2 = false) {
		super();

		if (isNaN(initData)) {
			this._initByData(initData.pathPointList, initData.options, initData.usage, generateUv2);
		} else {
			this._initByMaxVertex(initData, generateUv2);
		}
	}

	_initByMaxVertex(maxVertex, generateUv2) {
		this.setAttribute('position', new BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(DynamicDrawUsage));
		this.setAttribute('normal', new BufferAttribute(new Float32Array(maxVertex * 3), 3).setUsage(DynamicDrawUsage));
		this.setAttribute('uv', new BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(DynamicDrawUsage));
		if (generateUv2) {
			this.setAttribute('uv2', new BufferAttribute(new Float32Array(maxVertex * 2), 2).setUsage(DynamicDrawUsage));
		}

		this.drawRange.start = 0;
		this.drawRange.count = 0;

		this.setIndex(maxVertex > 65536 ?
			new Uint32BufferAttribute(maxVertex * 3, 1) :
			new Uint16BufferAttribute(maxVertex * 3, 1)
		);
	}

	_initByData(pathPointList, options = {}, usage, generateUv2) {
		const vertexData = generatePathVertexData(pathPointList, options, generateUv2);

		if (vertexData && vertexData.count !== 0) {
			this.setAttribute('position', new BufferAttribute(new Float32Array(vertexData.position), 3).setUsage(usage || StaticDrawUsage));
			this.setAttribute('normal', new BufferAttribute(new Float32Array(vertexData.normal), 3).setUsage(usage || StaticDrawUsage));
			this.setAttribute('uv', new BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(usage || StaticDrawUsage));
			if (generateUv2) {
				this.setAttribute('uv2', new BufferAttribute(new Float32Array(vertexData.uv2), 2).setUsage(usage || StaticDrawUsage));
			}

			this.setIndex((vertexData.position.length / 3) > 65536 ?
				new Uint32BufferAttribute(vertexData.indices, 1) :
				new Uint16BufferAttribute(vertexData.indices, 1)
			);
		} else {
			this._initByMaxVertex(2, generateUv2);
		}
	}

	/**
	 * Update geometry by PathPointList instance
	 * @param {PathPointList} pathPointList
	 * @param {Object} options
	 * @param {Number} [options.width=0.1]
	 * @param {Number} [options.progress=1]
	 * @param {Boolean} [options.arrow=true]
	 * @param {String} [options.side='both'] - "left"/"right"/"both"
	 */
	update(pathPointList, options = {}) {
		const generateUv2 = !!this.getAttribute('uv2');

		const vertexData = generatePathVertexData(pathPointList, options, generateUv2);

		if (vertexData) {
			this._updateAttributes(vertexData.position, vertexData.normal, vertexData.uv, generateUv2 ? vertexData.uv2 : null, vertexData.indices);
			this.drawRange.count = vertexData.count;
		} else {
			this.drawRange.count = 0;
		}
	}

	_resizeAttribute(name, len) {
		let attribute = this.getAttribute(name);
		while (attribute.array.length < len) {
			const oldLength = attribute.array.length;
			const newAttribute = new BufferAttribute(
				new Float32Array(oldLength * 2),
				attribute.itemSize,
				attribute.normalized
			);
			newAttribute.name = attribute.name;
			newAttribute.usage = attribute.usage;
			this.setAttribute(name, newAttribute);
			attribute = newAttribute;
		}
	}

	_resizeIndex(len) {
		let index = this.getIndex();
		while (index.array.length < len) {
			const oldLength = index.array.length;
			const newIndex = new BufferAttribute(
				oldLength * 2 > 65535 ? new Uint32Array(oldLength * 2) : new Uint16Array(oldLength * 2),
				1
			);
			newIndex.name = index.name;
			newIndex.usage = index.usage;
			this.setIndex(newIndex);
			index = newIndex;
		}
	}

	_updateAttributes(position, normal, uv, uv2, indices) {
		this._resizeAttribute('position', position.length);
		const positionAttribute = this.getAttribute('position');
		positionAttribute.array.set(position, 0);
		if (positionAttribute.addUpdateRange) {
			positionAttribute.clearUpdateRanges();
			positionAttribute.addUpdateRange(0, position.length);
		} else { // compatibility with r158 earlier
			positionAttribute.updateRange.count = position.length;
		}
		positionAttribute.needsUpdate = true;

		this._resizeAttribute('normal', normal.length);
		const normalAttribute = this.getAttribute('normal');
		normalAttribute.array.set(normal, 0);
		if (normalAttribute.addUpdateRange) {
			normalAttribute.clearUpdateRanges();
			normalAttribute.addUpdateRange(0, normal.length);
		} else { // compatibility with r158 earlier
			normalAttribute.updateRange.count = normal.length;
		}
		normalAttribute.needsUpdate = true;

		this._resizeAttribute('uv', uv.length);
		const uvAttribute = this.getAttribute('uv');
		uvAttribute.array.set(uv, 0);
		if (uvAttribute.addUpdateRange) {
			uvAttribute.clearUpdateRanges();
			uvAttribute.addUpdateRange(0, uv.length);
		} else { // compatibility with r158 earlier
			uvAttribute.updateRange.count = uv.length;
		}
		uvAttribute.needsUpdate = true;

		if (uv2) {
			this._resizeAttribute('uv2', uv2.length);
			const uv2Attribute = this.getAttribute('uv2');
			uv2Attribute.array.set(uv2, 0);
			if (uv2Attribute.addUpdateRange) {
				uv2Attribute.clearUpdateRanges();
				uv2Attribute.addUpdateRange(0, uv2.length);
			} else { // compatibility with r158 earlier
				uv2Attribute.updateRange.count = uv2.length;
			}
			uv2Attribute.needsUpdate = true;
		}

		this._resizeIndex(indices.length);
		const indexAttribute = this.getIndex();
		indexAttribute.set(indices, 0);
		if (indexAttribute.addUpdateRange) {
			indexAttribute.clearUpdateRanges();
			indexAttribute.addUpdateRange(0, indices.length);
		} else { // compatibility with r158 earlier
			indexAttribute.updateRange.count = indices.length;
		}
		indexAttribute.needsUpdate = true;
	}

}

// Vertex Data Generate Functions

function generatePathVertexData(pathPointList, options, generateUv2 = false) {
	const width = options.width || 0.1;
	const progress = options.progress !== undefined ? options.progress : 1;
	const arrow = options.arrow !== undefined ? options.arrow : true;
	const side = options.side !== undefined ? options.side : 'both';

	const halfWidth = width / 2;
	const sideWidth = (side !== 'both' ? width / 2 : width);
	const totalDistance = pathPointList.distance();
	const progressDistance = progress * totalDistance;
	if (totalDistance == 0) {
		return null;
	}

	const sharpUvOffset = halfWidth / sideWidth;
	const sharpUvOffset2 = halfWidth / totalDistance;

	let count = 0;

	// modify data
	const position = [];
	const normal = [];
	const uv = [];
	const uv2 = [];
	const indices = [];
	let verticesCount = 0;

	const right = new Vector3();
	const left = new Vector3();

	// for sharp corners
	const leftOffset = new Vector3();
	const rightOffset = new Vector3();
	const tempPoint1 = new Vector3();
	const tempPoint2 = new Vector3();

	function addVertices(pathPoint) {
		const first = position.length === 0;
		const sharpCorner = pathPoint.sharp && !first;

		const uvDist = pathPoint.dist / sideWidth;
		const uvDist2 = pathPoint.dist / totalDistance;

		const dir = pathPoint.dir;
		const up = pathPoint.up;
		const _right = pathPoint.right;

		if (side !== 'left') {
			right.copy(_right).multiplyScalar(halfWidth * pathPoint.widthScale);
		} else {
			right.set(0, 0, 0);
		}

		if (side !== 'right') {
			left.copy(_right).multiplyScalar(-halfWidth * pathPoint.widthScale);
		} else {
			left.set(0, 0, 0);
		}

		right.add(pathPoint.pos);
		left.add(pathPoint.pos);

		if (sharpCorner) {
			leftOffset.fromArray(position, position.length - 6).sub(left);
			rightOffset.fromArray(position, position.length - 3).sub(right);

			const leftDist = leftOffset.length();
			const rightDist = rightOffset.length();

			const sideOffset = leftDist - rightDist;
			let longerOffset, longEdge;

			if (sideOffset > 0) {
				longerOffset = leftOffset;
				longEdge = left;
			} else {
				longerOffset = rightOffset;
				longEdge = right;
			}

			tempPoint1.copy(longerOffset).setLength(Math.abs(sideOffset)).add(longEdge);

			const _cos = tempPoint2.copy(longEdge).sub(tempPoint1).normalize().dot(dir);
			const _len = tempPoint2.copy(longEdge).sub(tempPoint1).length();
			const _dist = _cos * _len * 2;

			tempPoint2.copy(dir).setLength(_dist).add(tempPoint1);

			if (sideOffset > 0) {
				position.push(
					tempPoint1.x, tempPoint1.y, tempPoint1.z, // 6
					right.x, right.y, right.z, // 5
					left.x, left.y, left.z, // 4
					right.x, right.y, right.z, // 3
					tempPoint2.x, tempPoint2.y, tempPoint2.z, // 2
					right.x, right.y, right.z // 1
				);

				verticesCount += 6;

				indices.push(
					verticesCount - 6, verticesCount - 8, verticesCount - 7,
					verticesCount - 6, verticesCount - 7, verticesCount - 5,

					verticesCount - 4, verticesCount - 6, verticesCount - 5,
					verticesCount - 2, verticesCount - 4, verticesCount - 1
				);

				count += 12;
			} else {
				position.push(
					left.x, left.y, left.z, // 6
					tempPoint1.x, tempPoint1.y, tempPoint1.z, // 5
					left.x, left.y, left.z, // 4
					right.x, right.y, right.z, // 3
					left.x, left.y, left.z, // 2
					tempPoint2.x, tempPoint2.y, tempPoint2.z // 1
				);

				verticesCount += 6;

				indices.push(
					verticesCount - 6, verticesCount - 8, verticesCount - 7,
					verticesCount - 6, verticesCount - 7, verticesCount - 5,

					verticesCount - 6, verticesCount - 5, verticesCount - 3,
					verticesCount - 2, verticesCount - 3, verticesCount - 1
				);

				count += 12;
			}

			normal.push(
				up.x, up.y, up.z,
				up.x, up.y, up.z,
				up.x, up.y, up.z,
				up.x, up.y, up.z,
				up.x, up.y, up.z,
				up.x, up.y, up.z
			);

			uv.push(
				uvDist - sharpUvOffset, 0,
				uvDist - sharpUvOffset, 1,
				uvDist, 0,
				uvDist, 1,
				uvDist + sharpUvOffset, 0,
				uvDist + sharpUvOffset, 1
			);

			if (generateUv2) {
				uv2.push(
					uvDist2 - sharpUvOffset2, 0,
					uvDist2 - sharpUvOffset2, 1,
					uvDist2, 0,
					uvDist2, 1,
					uvDist2 + sharpUvOffset2, 0,
					uvDist2 + sharpUvOffset2, 1
				);
			}
		} else {
			position.push(
				left.x, left.y, left.z,
				right.x, right.y, right.z
			);

			normal.push(
				up.x, up.y, up.z,
				up.x, up.y, up.z
			);

			uv.push(
				uvDist, 0,
				uvDist, 1
			);

			if (generateUv2) {
				uv2.push(
					uvDist2, 0,
					uvDist2, 1
				);
			}

			verticesCount += 2;

			if (!first) {
				indices.push(
					verticesCount - 2, verticesCount - 4, verticesCount - 3,
					verticesCount - 2, verticesCount - 3, verticesCount - 1
				);

				count += 6;
			}
		}
	}

	const sharp = new Vector3();
	function addStart(pathPoint) {
		const dir = pathPoint.dir;
		const up = pathPoint.up;
		const _right = pathPoint.right;

		const uvDist = pathPoint.dist / sideWidth;
		const uvDist2 = pathPoint.dist / totalDistance;

		if (side !== 'left') {
			right.copy(_right).multiplyScalar(halfWidth * 2);
		} else {
			right.set(0, 0, 0);
		}

		if (side !== 'right') {
			left.copy(_right).multiplyScalar(-halfWidth * 2);
		} else {
			left.set(0, 0, 0);
		}

		sharp.copy(dir).setLength(halfWidth * 3);

		right.add(pathPoint.pos);
		left.add(pathPoint.pos);
		sharp.add(pathPoint.pos);

		position.push(
			left.x, left.y, left.z,
			right.x, right.y, right.z,
			sharp.x, sharp.y, sharp.z
		);

		normal.push(
			up.x, up.y, up.z,
			up.x, up.y, up.z,
			up.x, up.y, up.z
		);

		uv.push(
			uvDist, side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
			uvDist, side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
			uvDist + 1.5, side !== 'both' ? 0 : 0.5
		);

		if (generateUv2) {
			uv2.push(
				uvDist2, side !== 'both' ? (side !== 'right' ? -2 : 0) : -0.5,
				uvDist2, side !== 'both' ? (side !== 'left' ? 2 : 0) : 1.5,
				uvDist2 + (1.5 * width / totalDistance), side !== 'both' ? 0 : 0.5
			);
		}

		verticesCount += 3;

		indices.push(
			verticesCount - 1, verticesCount - 3, verticesCount - 2
		);

		count += 3;
	}

	let lastPoint;

	if (progressDistance > 0) {
		for (let i = 0; i < pathPointList.count; i++) {
			const pathPoint = pathPointList.array[i];

			if (pathPoint.dist > progressDistance) {
				const prevPoint = pathPointList.array[i - 1];
				lastPoint = new PathPoint();

				// linear lerp for progress
				const alpha = (progressDistance - prevPoint.dist) / (pathPoint.dist - prevPoint.dist);
				lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);

				addVertices(lastPoint);
				break;
			} else {
				addVertices(pathPoint);
			}
		}
	} else {
		lastPoint = pathPointList.array[0];
	}

	// build arrow geometry
	if (arrow) {
		lastPoint = lastPoint || pathPointList.array[pathPointList.count - 1];
		addStart(lastPoint);
	}

	return {
		position,
		normal,
		uv,
		uv2,
		indices,
		count
	};
}

/**
 * PathTubeGeometry
 */
class PathTubeGeometry extends PathGeometry {

	/**
	 * @param {object|number} initData - If initData is number, geometry init by empty data and set it as the max vertex. If initData is Object, it contains pathPointList and options.
	 * @param {boolean} [generateUv2=false]
	 */
	constructor(initData = 1000, generateUv2 = false) {
		super(initData, generateUv2);
	}

	_initByData(pathPointList, options = {}, usage, generateUv2) {
		const vertexData = generateTubeVertexData(pathPointList, options, generateUv2);

		if (vertexData && vertexData.count !== 0) {
			this.setAttribute('position', new BufferAttribute(new Float32Array(vertexData.position), 3).setUsage(usage || StaticDrawUsage));
			this.setAttribute('normal', new BufferAttribute(new Float32Array(vertexData.normal), 3).setUsage(usage || StaticDrawUsage));
			this.setAttribute('uv', new BufferAttribute(new Float32Array(vertexData.uv), 2).setUsage(usage || StaticDrawUsage));
			if (generateUv2) {
				this.setAttribute('uv2', new BufferAttribute(new Float32Array(vertexData.uv2), 2).setUsage(usage || StaticDrawUsage));
			}

			this.setIndex((vertexData.position.length / 3) > 65536 ?
				new Uint32BufferAttribute(vertexData.indices, 1) :
				new Uint16BufferAttribute(vertexData.indices, 1)
			);
		} else {
			this._initByMaxVertex(2, generateUv2);
		}
	}

	/**
	 * Update geometry by PathPointList instance
	 * @param {PathPointList} pathPointList
	 * @param {Object} options
	 * @param {Number} [options.radius=0.1]
	 * @param {Number} [options.progress=1]
	 * @param {Boolean} [options.radialSegments=8]
	 * @param {String} [options.startRad=0]
	 */
	update(pathPointList, options = {}) {
		const generateUv2 = !!this.getAttribute('uv2');

		const vertexData = generateTubeVertexData(pathPointList, options, generateUv2);

		if (vertexData) {
			this._updateAttributes(vertexData.position, vertexData.normal, vertexData.uv, generateUv2 ? vertexData.uv2 : null, vertexData.indices);
			this.drawRange.count = vertexData.count;
		} else {
			this.drawRange.count = 0;
		}
	}

}

// Vertex Data Generate Functions

function generateTubeVertexData(pathPointList, options, generateUv2 = false) {
	const radius = options.radius || 0.1;
	const progress = options.progress !== undefined ? options.progress : 1;
	const radialSegments = Math.max(2, options.radialSegments || 8);
	const startRad = options.startRad || 0;

	const circum = radius * 2 * Math.PI;
	const totalDistance = pathPointList.distance();
	const progressDistance = progress * totalDistance;
	if (progressDistance == 0) {
		return null;
	}

	let count = 0;

	// modify data
	const position = [];
	const normal = [];
	const uv = [];
	const uv2 = [];
	const indices = [];
	let verticesCount = 0;

	const normalDir = new Vector3();
	function addVertices(pathPoint, radius, radialSegments) {
		const first = position.length === 0;
		const uvDist = pathPoint.dist / circum;
		const uvDist2 = pathPoint.dist / totalDistance;

		for (let r = 0; r <= radialSegments; r++) {
			let _r = r;
			if (_r == radialSegments) {
				_r = 0;
			}
			normalDir.copy(pathPoint.up).applyAxisAngle(pathPoint.dir, startRad + Math.PI * 2 * _r / radialSegments).normalize();

			position.push(pathPoint.pos.x + normalDir.x * radius * pathPoint.widthScale, pathPoint.pos.y + normalDir.y * radius * pathPoint.widthScale, pathPoint.pos.z + normalDir.z * radius * pathPoint.widthScale);
			normal.push(normalDir.x, normalDir.y, normalDir.z);
			uv.push(uvDist, r / radialSegments);

			if (generateUv2) {
				uv2.push(uvDist2, r / radialSegments);
			}

			verticesCount++;
		}

		if (!first) {
			const begin1 = verticesCount - (radialSegments + 1) * 2;
			const begin2 = verticesCount - (radialSegments + 1);

			for (let i = 0; i < radialSegments; i++) {
				indices.push(
					begin2 + i, begin1 + i, begin1 + i + 1,
					begin2 + i, begin1 + i + 1, begin2 + i + 1
				);

				count += 6;
			}
		}
	}

	if (progressDistance > 0) {
		for (let i = 0; i < pathPointList.count; i++) {
			const pathPoint = pathPointList.array[i];

			if (pathPoint.dist > progressDistance) {
				const prevPoint = pathPointList.array[i - 1];
				const lastPoint = new PathPoint();

				// linear lerp for progress
				const alpha = (progressDistance - prevPoint.dist) / (pathPoint.dist - prevPoint.dist);
				lastPoint.lerpPathPoints(prevPoint, pathPoint, alpha);

				addVertices(lastPoint, radius, radialSegments);
				break;
			} else {
				addVertices(pathPoint, radius, radialSegments);
			}
		}
	}

	return {
		position,
		normal,
		uv,
		uv2,
		indices,
		count
	};
}

export { PathGeometry, PathPointList, PathTubeGeometry };