greasedLineTools.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. import { Curve3 } from "../Maths/math.path.js";
  2. import { VertexBuffer } from "../Buffers/buffer.js";
  3. import { TmpVectors, Vector3 } from "../Maths/math.vector.js";
  4. import { CreateTextShapePaths } from "../Meshes/Builders/textBuilder.js";
  5. import { RawTexture } from "../Materials/Textures/rawTexture.js";
  6. import { Engine } from "../Engines/engine.js";
  7. import { GreasedLineMaterialDefaults } from "../Materials/GreasedLine/greasedLineMaterialDefaults.js";
  8. /**
  9. * Tool functions for GreasedLine
  10. */
  11. export class GreasedLineTools {
  12. /**
  13. * Converts GreasedLinePoints to number[][]
  14. * @param points GreasedLinePoints
  15. * @returns number[][] with x, y, z coordinates of the points, like [[x, y, z, x, y, z, ...], [x, y, z, ...]]
  16. */
  17. static ConvertPoints(points) {
  18. if (points.length && Array.isArray(points) && typeof points[0] === "number") {
  19. return [points];
  20. }
  21. else if (points.length && Array.isArray(points[0]) && typeof points[0][0] === "number") {
  22. return points;
  23. }
  24. else if (points.length && !Array.isArray(points[0]) && points[0] instanceof Vector3) {
  25. const positions = [];
  26. for (let j = 0; j < points.length; j++) {
  27. const p = points[j];
  28. positions.push(p.x, p.y, p.z);
  29. }
  30. return [positions];
  31. }
  32. else if (points.length > 0 && Array.isArray(points[0]) && points[0].length > 0 && points[0][0] instanceof Vector3) {
  33. const positions = [];
  34. const vectorPoints = points;
  35. vectorPoints.forEach((p) => {
  36. positions.push(p.flatMap((p2) => [p2.x, p2.y, p2.z]));
  37. });
  38. return positions;
  39. }
  40. else if (points instanceof Float32Array) {
  41. return [Array.from(points)];
  42. }
  43. else if (points.length && points[0] instanceof Float32Array) {
  44. const positions = [];
  45. points.forEach((p) => {
  46. positions.push(Array.from(p));
  47. });
  48. return positions;
  49. }
  50. return [];
  51. }
  52. /**
  53. * Omit zero length lines predicate for the MeshesToLines function
  54. * @param p1 point1 position of the face
  55. * @param p2 point2 position of the face
  56. * @param p3 point3 position of the face
  57. * @returns original points or null if any edge length is zero
  58. */
  59. static OmitZeroLengthPredicate(p1, p2, p3) {
  60. const fileredPoints = [];
  61. // edge1
  62. if (p2.subtract(p1).lengthSquared() > 0) {
  63. fileredPoints.push([p1, p2]);
  64. }
  65. // edge2
  66. if (p3.subtract(p2).lengthSquared() > 0) {
  67. fileredPoints.push([p2, p3]);
  68. }
  69. // edge3
  70. if (p1.subtract(p3).lengthSquared() > 0) {
  71. fileredPoints.push([p3, p1]);
  72. }
  73. return fileredPoints.length === 0 ? null : fileredPoints;
  74. }
  75. /**
  76. * Omit duplicate lines predicate for the MeshesToLines function
  77. * @param p1 point1 position of the face
  78. * @param p2 point2 position of the face
  79. * @param p3 point3 position of the face
  80. * @param points array of points to search in
  81. * @returns original points or null if any edge length is zero
  82. */
  83. static OmitDuplicatesPredicate(p1, p2, p3, points) {
  84. const fileredPoints = [];
  85. // edge1
  86. if (!GreasedLineTools._SearchInPoints(p1, p2, points)) {
  87. fileredPoints.push([p1, p2]);
  88. }
  89. // edge2
  90. if (!GreasedLineTools._SearchInPoints(p2, p3, points)) {
  91. fileredPoints.push([p2, p3]);
  92. }
  93. // edge3
  94. if (!GreasedLineTools._SearchInPoints(p3, p1, points)) {
  95. fileredPoints.push([p3, p1]);
  96. }
  97. return fileredPoints.length === 0 ? null : fileredPoints;
  98. }
  99. static _SearchInPoints(p1, p2, points) {
  100. for (const ps of points) {
  101. for (let i = 0; i < ps.length; i++) {
  102. if (ps[i]?.equals(p1)) {
  103. // find the first point
  104. // if it has a sibling of p2 the line already exists
  105. if (ps[i + 1]?.equals(p2) || ps[i - 1]?.equals(p2)) {
  106. return true;
  107. }
  108. }
  109. }
  110. }
  111. return false;
  112. }
  113. /**
  114. * Gets mesh triangles as line positions
  115. * @param meshes array of meshes
  116. * @param predicate predicate function which decides whether to include the mesh triangle/face in the ouput
  117. * @returns array of arrays of points
  118. */
  119. static MeshesToLines(meshes, predicate) {
  120. const points = [];
  121. meshes.forEach((m, meshIndex) => {
  122. const vertices = m.getVerticesData(VertexBuffer.PositionKind);
  123. const indices = m.getIndices();
  124. if (vertices && indices) {
  125. for (let i = 0, ii = 0; i < indices.length; i++) {
  126. const vi1 = indices[ii++] * 3;
  127. const vi2 = indices[ii++] * 3;
  128. const vi3 = indices[ii++] * 3;
  129. const p1 = new Vector3(vertices[vi1], vertices[vi1 + 1], vertices[vi1 + 2]);
  130. const p2 = new Vector3(vertices[vi2], vertices[vi2 + 1], vertices[vi2 + 2]);
  131. const p3 = new Vector3(vertices[vi3], vertices[vi3 + 1], vertices[vi3 + 2]);
  132. if (predicate) {
  133. const pointsFromPredicate = predicate(p1, p2, p3, points, i, vi1, m, meshIndex, vertices, indices);
  134. if (pointsFromPredicate) {
  135. for (const p of pointsFromPredicate) {
  136. points.push(p);
  137. }
  138. }
  139. }
  140. else {
  141. points.push([p1, p2], [p2, p3], [p3, p1]);
  142. }
  143. }
  144. }
  145. });
  146. return points;
  147. }
  148. /**
  149. * Converts number coordinates to Vector3s
  150. * @param points number array of x, y, z, x, y z, ... coordinates
  151. * @returns Vector3 array
  152. */
  153. static ToVector3Array(points) {
  154. if (Array.isArray(points[0])) {
  155. const array = [];
  156. const inputArray = points;
  157. for (const subInputArray of inputArray) {
  158. const subArray = [];
  159. for (let i = 0; i < subInputArray.length; i += 3) {
  160. subArray.push(new Vector3(subInputArray[i], subInputArray[i + 1], subInputArray[i + 2]));
  161. }
  162. array.push(subArray);
  163. }
  164. return array;
  165. }
  166. const inputArray = points;
  167. const array = [];
  168. for (let i = 0; i < inputArray.length; i += 3) {
  169. array.push(new Vector3(inputArray[i], inputArray[i + 1], inputArray[i + 2]));
  170. }
  171. return array;
  172. }
  173. /**
  174. * Gets a number array from a Vector3 array.
  175. * You can you for example to convert your Vector3[] offsets to the required number[] for the offsets option.
  176. * @param points Vector3 array
  177. * @returns an array of x, y, z coordinates as numbers [x, y, z, x, y, z, x, y, z, ....]
  178. */
  179. static ToNumberArray(points) {
  180. return points.flatMap((v) => [v.x, v.y, v.z]);
  181. }
  182. /**
  183. * Calculates the sum of points of every line and the number of points in each line.
  184. * This function is useful when you are drawing multiple lines in one mesh and you want
  185. * to know the counts. For example for creating an offsets table.
  186. * @param points point array
  187. * @returns points count info
  188. */
  189. static GetPointsCountInfo(points) {
  190. const counts = new Array(points.length);
  191. let total = 0;
  192. for (let n = points.length; n--;) {
  193. counts[n] = points[n].length / 3;
  194. total += counts[n];
  195. }
  196. return { total, counts };
  197. }
  198. /**
  199. * Gets the length of the line counting all it's segments length
  200. * @param data array of line points
  201. * @returns length of the line
  202. */
  203. static GetLineLength(data) {
  204. if (data.length === 0) {
  205. return 0;
  206. }
  207. let points;
  208. if (typeof data[0] === "number") {
  209. points = GreasedLineTools.ToVector3Array(data);
  210. }
  211. else {
  212. points = data;
  213. }
  214. const tmp = TmpVectors.Vector3[0];
  215. let length = 0;
  216. for (let index = 0; index < points.length - 1; index++) {
  217. const point1 = points[index];
  218. const point2 = points[index + 1];
  219. length += point2.subtractToRef(point1, tmp).length();
  220. }
  221. return length;
  222. }
  223. /**
  224. * Gets the the length from the beginning to each point of the line as array.
  225. * @param data array of line points
  226. * @returns length array of the line
  227. */
  228. static GetLineLengthArray(data) {
  229. const out = new Float32Array(data.length / 3);
  230. let length = 0;
  231. for (let index = 0, pointsLength = data.length / 3 - 1; index < pointsLength; index++) {
  232. let x = data[index * 3 + 0];
  233. let y = data[index * 3 + 1];
  234. let z = data[index * 3 + 2];
  235. x -= data[index * 3 + 3];
  236. y -= data[index * 3 + 4];
  237. z -= data[index * 3 + 5];
  238. const currentLength = Math.sqrt(x * x + y * y + z * z);
  239. length += currentLength;
  240. out[index + 1] = length;
  241. }
  242. return out;
  243. }
  244. /**
  245. * Divides a segment into smaller segments.
  246. * A segment is a part of the line between it's two points.
  247. * @param point1 first point of the line
  248. * @param point2 second point of the line
  249. * @param segmentCount number of segments we want to have in the divided line
  250. * @returns
  251. */
  252. static SegmentizeSegmentByCount(point1, point2, segmentCount) {
  253. const dividedLinePoints = [];
  254. const diff = point2.subtract(point1);
  255. const divisor = TmpVectors.Vector3[0];
  256. divisor.setAll(segmentCount);
  257. const segmentVector = TmpVectors.Vector3[1];
  258. diff.divideToRef(divisor, segmentVector);
  259. let nextPoint = point1.clone();
  260. dividedLinePoints.push(nextPoint);
  261. for (let index = 0; index < segmentCount; index++) {
  262. nextPoint = nextPoint.clone();
  263. dividedLinePoints.push(nextPoint.addInPlace(segmentVector));
  264. }
  265. return dividedLinePoints;
  266. }
  267. /**
  268. * Divides a line into segments.
  269. * A segment is a part of the line between it's two points.
  270. * @param what line points
  271. * @param segmentLength length of each segment of the resulting line (distance between two line points)
  272. * @returns line point
  273. */
  274. static SegmentizeLineBySegmentLength(what, segmentLength) {
  275. const subLines = what[0] instanceof Vector3
  276. ? GreasedLineTools.GetLineSegments(what)
  277. : typeof what[0] === "number"
  278. ? GreasedLineTools.GetLineSegments(GreasedLineTools.ToVector3Array(what))
  279. : what;
  280. const points = [];
  281. subLines.forEach((s) => {
  282. if (s.length > segmentLength) {
  283. const segments = GreasedLineTools.SegmentizeSegmentByCount(s.point1, s.point2, Math.ceil(s.length / segmentLength));
  284. segments.forEach((seg) => {
  285. points.push(seg);
  286. });
  287. }
  288. else {
  289. points.push(s.point1);
  290. points.push(s.point2);
  291. }
  292. });
  293. return points;
  294. }
  295. /**
  296. * Divides a line into segments.
  297. * A segment is a part of the line between it's two points.
  298. * @param what line points
  299. * @param segmentCount number of segments
  300. * @returns line point
  301. */
  302. static SegmentizeLineBySegmentCount(what, segmentCount) {
  303. const points = (typeof what[0] === "number" ? GreasedLineTools.ToVector3Array(what) : what);
  304. const segmentLength = GreasedLineTools.GetLineLength(points) / segmentCount;
  305. return GreasedLineTools.SegmentizeLineBySegmentLength(points, segmentLength);
  306. }
  307. /**
  308. * Gets line segments.
  309. * A segment is a part of the line between it's two points.
  310. * @param points line points
  311. * @returns segments information of the line segment including starting point, ending point and the distance between them
  312. */
  313. static GetLineSegments(points) {
  314. const segments = [];
  315. for (let index = 0; index < points.length - 1; index++) {
  316. const point1 = points[index];
  317. const point2 = points[index + 1];
  318. const length = point2.subtract(point1).length();
  319. segments.push({ point1, point2, length });
  320. }
  321. return segments;
  322. }
  323. /**
  324. * Gets the minimum and the maximum length of a line segment in the line.
  325. * A segment is a part of the line between it's two points.
  326. * @param points line points
  327. * @returns
  328. */
  329. static GetMinMaxSegmentLength(points) {
  330. const subLines = GreasedLineTools.GetLineSegments(points);
  331. const sorted = subLines.sort((s) => s.length);
  332. return {
  333. min: sorted[0].length,
  334. max: sorted[sorted.length - 1].length,
  335. };
  336. }
  337. /**
  338. * Finds the last visible position in world space of the line according to the visibility parameter
  339. * @param lineSegments segments of the line
  340. * @param lineLength total length of the line
  341. * @param visbility normalized value of visibility
  342. * @param localSpace if true the result will be in local space (default is false)
  343. * @returns world space coordinate of the last visible piece of the line
  344. */
  345. static GetPositionOnLineByVisibility(lineSegments, lineLength, visbility, localSpace = false) {
  346. const lengthVisibilityRatio = lineLength * visbility;
  347. let sumSegmentLengths = 0;
  348. let segmentIndex = 0;
  349. const lineSegmentsLength = lineSegments.length;
  350. for (let i = 0; i < lineSegmentsLength; i++) {
  351. if (lengthVisibilityRatio <= sumSegmentLengths + lineSegments[i].length) {
  352. segmentIndex = i;
  353. break;
  354. }
  355. sumSegmentLengths += lineSegments[i].length;
  356. }
  357. const s = (lengthVisibilityRatio - sumSegmentLengths) / lineSegments[segmentIndex].length;
  358. lineSegments[segmentIndex].point2.subtractToRef(lineSegments[segmentIndex].point1, TmpVectors.Vector3[0]);
  359. TmpVectors.Vector3[1] = TmpVectors.Vector3[0].multiplyByFloats(s, s, s);
  360. if (!localSpace) {
  361. TmpVectors.Vector3[1].addInPlace(lineSegments[segmentIndex].point1);
  362. }
  363. return TmpVectors.Vector3[1].clone();
  364. }
  365. /**
  366. * Creates lines in a shape of circle/arc.
  367. * A segment is a part of the line between it's two points.
  368. * @param radiusX radiusX of the circle
  369. * @param segments number of segments in the circle
  370. * @param z z coordinate of the points. Defaults to 0.
  371. * @param radiusY radiusY of the circle - you can draw an oval if using different values
  372. * @param segmentAngle angle offset of the segments. Defaults to Math.PI * 2 / segments. Change this value to draw a part of the circle.
  373. * @returns line points
  374. */
  375. static GetCircleLinePoints(radiusX, segments, z = 0, radiusY = radiusX, segmentAngle = (Math.PI * 2) / segments) {
  376. const points = [];
  377. for (let i = 0; i <= segments; i++) {
  378. points.push(new Vector3(Math.cos(i * segmentAngle) * radiusX, Math.sin(i * segmentAngle) * radiusY, z));
  379. }
  380. return points;
  381. }
  382. /**
  383. * Gets line points in a shape of a bezier curve
  384. * @param p0 bezier point0
  385. * @param p1 bezier point1
  386. * @param p2 bezier point2
  387. * @param segments number of segments in the curve
  388. * @returns
  389. */
  390. static GetBezierLinePoints(p0, p1, p2, segments) {
  391. return Curve3.CreateQuadraticBezier(p0, p1, p2, segments)
  392. .getPoints()
  393. .flatMap((v) => [v.x, v.y, v.z]);
  394. }
  395. /**
  396. *
  397. * @param position position of the arrow cap (mainly you want to create a triangle, set widthUp and widthDown to the same value and omit widthStartUp and widthStartDown)
  398. * @param direction direction which the arrow points to
  399. * @param length length (size) of the arrow cap itself
  400. * @param widthUp the arrow width above the line
  401. * @param widthDown the arrow width belove the line
  402. * @param widthStartUp the arrow width at the start of the arrow above the line. In most scenarios this is 0.
  403. * @param widthStartDown the arrow width at the start of the arrow below the line. In most scenarios this is 0.
  404. * @returns
  405. */
  406. static GetArrowCap(position, direction, length, widthUp, widthDown, widthStartUp = 0, widthStartDown = 0) {
  407. const points = [position.clone(), position.add(direction.multiplyByFloats(length, length, length))];
  408. const widths = [widthUp, widthDown, widthStartUp, widthStartDown];
  409. return {
  410. points,
  411. widths,
  412. };
  413. }
  414. /**
  415. * Gets 3D positions of points from a text and font
  416. * @param text Text
  417. * @param size Size of the font
  418. * @param resolution Resolution of the font
  419. * @param fontData defines the font data (can be generated with http://gero3.github.io/facetype.js/)
  420. * @param z z coordinate
  421. * @param includeInner include the inner parts of the font in the result. Default true. If false, only the outlines will be returned.
  422. * @returns number[][] of 3D positions
  423. */
  424. static GetPointsFromText(text, size, resolution, fontData, z = 0, includeInner = true) {
  425. const allPoints = [];
  426. const shapePaths = CreateTextShapePaths(text, size, resolution, fontData);
  427. for (const sp of shapePaths) {
  428. for (const p of sp.paths) {
  429. const points = [];
  430. const points2d = p.getPoints();
  431. for (const p2d of points2d) {
  432. points.push(p2d.x, p2d.y, z);
  433. }
  434. allPoints.push(points);
  435. }
  436. if (includeInner) {
  437. for (const h of sp.holes) {
  438. const holes = [];
  439. const points2d = h.getPoints();
  440. for (const p2d of points2d) {
  441. holes.push(p2d.x, p2d.y, z);
  442. }
  443. allPoints.push(holes);
  444. }
  445. }
  446. }
  447. return allPoints;
  448. }
  449. /**
  450. * Converts an array of Color3 to Uint8Array
  451. * @param colors Arrray of Color3
  452. * @returns Uin8Array of colors [r, g, b, a, r, g, b, a, ...]
  453. */
  454. static Color3toRGBAUint8(colors) {
  455. const colorTable = new Uint8Array(colors.length * 4);
  456. for (let i = 0, j = 0; i < colors.length; i++) {
  457. colorTable[j++] = colors[i].r * 255;
  458. colorTable[j++] = colors[i].g * 255;
  459. colorTable[j++] = colors[i].b * 255;
  460. colorTable[j++] = 255;
  461. }
  462. return colorTable;
  463. }
  464. /**
  465. * Creates a RawTexture from an RGBA color array and sets it on the plugin material instance.
  466. * @param name name of the texture
  467. * @param colors Uint8Array of colors
  468. * @param colorsSampling sampling mode of the created texture
  469. * @param scene Scene
  470. * @returns the colors texture
  471. */
  472. static CreateColorsTexture(name, colors, colorsSampling, scene) {
  473. const colorsArray = GreasedLineTools.Color3toRGBAUint8(colors);
  474. const colorsTexture = new RawTexture(colorsArray, colors.length, 1, Engine.TEXTUREFORMAT_RGBA, scene, false, true, colorsSampling);
  475. colorsTexture.name = name;
  476. return colorsTexture;
  477. }
  478. /**
  479. * A minimum size texture for the colors sampler2D when there is no colors texture defined yet.
  480. * For fast switching using the useColors property without the need to use defines.
  481. * @param scene Scene
  482. * @returns empty colors texture
  483. */
  484. static PrepareEmptyColorsTexture(scene) {
  485. if (!GreasedLineMaterialDefaults.EmptyColorsTexture) {
  486. const colorsArray = new Uint8Array(4);
  487. GreasedLineMaterialDefaults.EmptyColorsTexture = new RawTexture(colorsArray, 1, 1, Engine.TEXTUREFORMAT_RGBA, scene, false, false, RawTexture.NEAREST_NEAREST);
  488. GreasedLineMaterialDefaults.EmptyColorsTexture.name = "grlEmptyColorsTexture";
  489. }
  490. return GreasedLineMaterialDefaults.EmptyColorsTexture;
  491. }
  492. /**
  493. * Diposes the shared empty colors texture
  494. */
  495. static DisposeEmptyColorsTexture() {
  496. GreasedLineMaterialDefaults.EmptyColorsTexture?.dispose();
  497. GreasedLineMaterialDefaults.EmptyColorsTexture = null;
  498. }
  499. /**
  500. * Converts boolean to number.
  501. * @param bool the bool value
  502. * @returns 1 if true, 0 if false.
  503. */
  504. static BooleanToNumber(bool) {
  505. return bool ? 1 : 0;
  506. }
  507. }
  508. //# sourceMappingURL=greasedLineTools.js.map