textBuilder.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { Path2 } from "../../Maths/math.path.js";
  2. import { Vector3 } from "../../Maths/math.vector.js";
  3. import { Mesh } from "../mesh.js";
  4. import { TransformNode } from "../transformNode.js";
  5. import { ExtrudePolygon } from "./polygonBuilder.js";
  6. // Shape functions
  7. class ShapePath {
  8. /** Create the ShapePath used to support glyphs
  9. * @param resolution defines the resolution used to determine the number of points per curve (default is 4)
  10. */
  11. constructor(resolution) {
  12. this._paths = [];
  13. this._tempPaths = [];
  14. this._holes = [];
  15. this._resolution = resolution;
  16. }
  17. /** Move the virtual cursor to a coordinate
  18. * @param x defines the x coordinate
  19. * @param y defines the y coordinate
  20. */
  21. moveTo(x, y) {
  22. this._currentPath = new Path2(x, y);
  23. this._tempPaths.push(this._currentPath);
  24. }
  25. /** Draw a line from the virtual cursor to a given coordinate
  26. * @param x defines the x coordinate
  27. * @param y defines the y coordinate
  28. */
  29. lineTo(x, y) {
  30. this._currentPath.addLineTo(x, y);
  31. }
  32. /** Create a quadratic curve from the virtual cursor to a given coordinate
  33. * @param cpx defines the x coordinate of the control point
  34. * @param cpy defines the y coordinate of the control point
  35. * @param x defines the x coordinate of the end point
  36. * @param y defines the y coordinate of the end point
  37. */
  38. quadraticCurveTo(cpx, cpy, x, y) {
  39. this._currentPath.addQuadraticCurveTo(cpx, cpy, x, y, this._resolution);
  40. }
  41. /**
  42. * Create a bezier curve from the virtual cursor to a given coordinate
  43. * @param cpx1 defines the x coordinate of the first control point
  44. * @param cpy1 defines the y coordinate of the first control point
  45. * @param cpx2 defines the x coordinate of the second control point
  46. * @param cpy2 defines the y coordinate of the second control point
  47. * @param x defines the x coordinate of the end point
  48. * @param y defines the y coordinate of the end point
  49. */
  50. bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y) {
  51. this._currentPath.addBezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y, this._resolution);
  52. }
  53. /** Extract holes based on CW / CCW */
  54. extractHoles() {
  55. for (const path of this._tempPaths) {
  56. if (path.area() > 0) {
  57. this._holes.push(path);
  58. }
  59. else {
  60. this._paths.push(path);
  61. }
  62. }
  63. if (!this._paths.length && this._holes.length) {
  64. const temp = this._holes;
  65. this._holes = this._paths;
  66. this._paths = temp;
  67. }
  68. this._tempPaths.length = 0;
  69. }
  70. /** Gets the list of paths */
  71. get paths() {
  72. return this._paths;
  73. }
  74. /** Gets the list of holes */
  75. get holes() {
  76. return this._holes;
  77. }
  78. }
  79. // Utility functions
  80. function CreateShapePath(char, scale, offsetX, offsetY, resolution, fontData) {
  81. const glyph = fontData.glyphs[char] || fontData.glyphs["?"];
  82. if (!glyph) {
  83. // return if there is no glyph data
  84. return null;
  85. }
  86. const shapePath = new ShapePath(resolution);
  87. if (glyph.o) {
  88. const outline = glyph.o.split(" ");
  89. for (let i = 0, l = outline.length; i < l;) {
  90. const action = outline[i++];
  91. switch (action) {
  92. case "m": {
  93. // moveTo
  94. const x = parseInt(outline[i++]) * scale + offsetX;
  95. const y = parseInt(outline[i++]) * scale + offsetY;
  96. shapePath.moveTo(x, y);
  97. break;
  98. }
  99. case "l": {
  100. // lineTo
  101. const x = parseInt(outline[i++]) * scale + offsetX;
  102. const y = parseInt(outline[i++]) * scale + offsetY;
  103. shapePath.lineTo(x, y);
  104. break;
  105. }
  106. case "q": {
  107. // quadraticCurveTo
  108. const cpx = parseInt(outline[i++]) * scale + offsetX;
  109. const cpy = parseInt(outline[i++]) * scale + offsetY;
  110. const cpx1 = parseInt(outline[i++]) * scale + offsetX;
  111. const cpy1 = parseInt(outline[i++]) * scale + offsetY;
  112. shapePath.quadraticCurveTo(cpx1, cpy1, cpx, cpy);
  113. break;
  114. }
  115. case "b": {
  116. // bezierCurveTo
  117. const cpx = parseInt(outline[i++]) * scale + offsetX;
  118. const cpy = parseInt(outline[i++]) * scale + offsetY;
  119. const cpx1 = parseInt(outline[i++]) * scale + offsetX;
  120. const cpy1 = parseInt(outline[i++]) * scale + offsetY;
  121. const cpx2 = parseInt(outline[i++]) * scale + offsetX;
  122. const cpy2 = parseInt(outline[i++]) * scale + offsetY;
  123. shapePath.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, cpx, cpy);
  124. break;
  125. }
  126. }
  127. }
  128. }
  129. // Extract holes (based on clockwise data)
  130. shapePath.extractHoles();
  131. return { offsetX: glyph.ha * scale, shapePath: shapePath };
  132. }
  133. /**
  134. * Creates shape paths from a text and font
  135. * @param text the text
  136. * @param size size of the font
  137. * @param resolution resolution of the font
  138. * @param fontData defines the font data (can be generated with http://gero3.github.io/facetype.js/)
  139. * @returns array of ShapePath objects
  140. */
  141. export function CreateTextShapePaths(text, size, resolution, fontData) {
  142. const chars = Array.from(text);
  143. const scale = size / fontData.resolution;
  144. const line_height = (fontData.boundingBox.yMax - fontData.boundingBox.yMin + fontData.underlineThickness) * scale;
  145. const shapePaths = [];
  146. let offsetX = 0, offsetY = 0;
  147. for (let i = 0; i < chars.length; i++) {
  148. const char = chars[i];
  149. if (char === "\n") {
  150. offsetX = 0;
  151. offsetY -= line_height;
  152. }
  153. else {
  154. const ret = CreateShapePath(char, scale, offsetX, offsetY, resolution, fontData);
  155. if (ret) {
  156. offsetX += ret.offsetX;
  157. shapePaths.push(ret.shapePath);
  158. }
  159. }
  160. }
  161. return shapePaths;
  162. }
  163. /**
  164. * Create a text mesh
  165. * @param name defines the name of the mesh
  166. * @param text defines the text to use to build the mesh
  167. * @param fontData defines the font data (can be generated with http://gero3.github.io/facetype.js/)
  168. * @param options defines options used to create the mesh
  169. * @param scene defines the hosting scene
  170. * @param earcutInjection can be used to inject your own earcut reference
  171. * @returns a new Mesh
  172. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set/text
  173. */
  174. export function CreateText(name, text, fontData, options = {
  175. size: 50,
  176. resolution: 8,
  177. depth: 1.0,
  178. }, scene = null, earcutInjection = earcut) {
  179. // First we need to generate the paths
  180. const shapePaths = CreateTextShapePaths(text, options.size || 50, options.resolution || 8, fontData);
  181. // And extrude them
  182. const meshes = [];
  183. let letterIndex = 0;
  184. for (const shapePath of shapePaths) {
  185. if (!shapePath.paths.length) {
  186. continue;
  187. }
  188. const holes = shapePath.holes.slice(); // Copy it as we will update the copy
  189. for (const path of shapePath.paths) {
  190. const holeVectors = [];
  191. const shapeVectors = [];
  192. const points = path.getPoints();
  193. for (const point of points) {
  194. shapeVectors.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
  195. }
  196. // Holes
  197. const localHolesCopy = holes.slice();
  198. for (const hole of localHolesCopy) {
  199. const points = hole.getPoints();
  200. let found = false;
  201. for (const point of points) {
  202. if (path.isPointInside(point)) {
  203. found = true;
  204. break;
  205. }
  206. }
  207. if (!found) {
  208. continue;
  209. }
  210. const holePoints = [];
  211. for (const point of points) {
  212. holePoints.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
  213. }
  214. holeVectors.push(holePoints);
  215. // Remove the hole as it was already used
  216. holes.splice(holes.indexOf(hole), 1);
  217. }
  218. // There is at least a hole but it was unaffected
  219. if (!holeVectors.length && holes.length) {
  220. for (const hole of holes) {
  221. const points = hole.getPoints();
  222. const holePoints = [];
  223. for (const point of points) {
  224. holePoints.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
  225. }
  226. holeVectors.push(holePoints);
  227. }
  228. }
  229. // Extrusion!
  230. const mesh = ExtrudePolygon(name, {
  231. shape: shapeVectors,
  232. holes: holeVectors.length ? holeVectors : undefined,
  233. depth: options.depth || 1.0,
  234. faceUV: options.faceUV || options.perLetterFaceUV?.(letterIndex),
  235. faceColors: options.faceColors || options.perLetterFaceColors?.(letterIndex),
  236. sideOrientation: Mesh._GetDefaultSideOrientation(options.sideOrientation || Mesh.DOUBLESIDE),
  237. }, scene, earcutInjection);
  238. meshes.push(mesh);
  239. letterIndex++;
  240. }
  241. }
  242. // Then we can merge everyone into one single mesh
  243. const newMesh = Mesh.MergeMeshes(meshes, true, true);
  244. if (newMesh) {
  245. // Move pivot to desired center / bottom / center position
  246. const bbox = newMesh.getBoundingInfo().boundingBox;
  247. newMesh.position.x += -(bbox.minimumWorld.x + bbox.maximumWorld.x) / 2; // Mid X
  248. newMesh.position.y += -(bbox.minimumWorld.y + bbox.maximumWorld.y) / 2; // Mid Z as it will rotate
  249. newMesh.position.z += -(bbox.minimumWorld.z + bbox.maximumWorld.z) / 2 + bbox.extendSize.z; // Bottom Y as it will rotate
  250. newMesh.name = name;
  251. // Rotate 90° Up
  252. const pivot = new TransformNode("pivot", scene);
  253. pivot.rotation.x = -Math.PI / 2;
  254. newMesh.parent = pivot;
  255. newMesh.bakeCurrentTransformIntoVertices();
  256. // Remove the pivot
  257. newMesh.parent = null;
  258. pivot.dispose();
  259. }
  260. return newMesh;
  261. }
  262. //# sourceMappingURL=textBuilder.js.map