123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- import { Path2 } from "../../Maths/math.path.js";
- import { Vector3 } from "../../Maths/math.vector.js";
- import { Mesh } from "../mesh.js";
- import { TransformNode } from "../transformNode.js";
- import { ExtrudePolygon } from "./polygonBuilder.js";
- // Shape functions
- class ShapePath {
- /** Create the ShapePath used to support glyphs
- * @param resolution defines the resolution used to determine the number of points per curve (default is 4)
- */
- constructor(resolution) {
- this._paths = [];
- this._tempPaths = [];
- this._holes = [];
- this._resolution = resolution;
- }
- /** Move the virtual cursor to a coordinate
- * @param x defines the x coordinate
- * @param y defines the y coordinate
- */
- moveTo(x, y) {
- this._currentPath = new Path2(x, y);
- this._tempPaths.push(this._currentPath);
- }
- /** Draw a line from the virtual cursor to a given coordinate
- * @param x defines the x coordinate
- * @param y defines the y coordinate
- */
- lineTo(x, y) {
- this._currentPath.addLineTo(x, y);
- }
- /** Create a quadratic curve from the virtual cursor to a given coordinate
- * @param cpx defines the x coordinate of the control point
- * @param cpy defines the y coordinate of the control point
- * @param x defines the x coordinate of the end point
- * @param y defines the y coordinate of the end point
- */
- quadraticCurveTo(cpx, cpy, x, y) {
- this._currentPath.addQuadraticCurveTo(cpx, cpy, x, y, this._resolution);
- }
- /**
- * Create a bezier curve from the virtual cursor to a given coordinate
- * @param cpx1 defines the x coordinate of the first control point
- * @param cpy1 defines the y coordinate of the first control point
- * @param cpx2 defines the x coordinate of the second control point
- * @param cpy2 defines the y coordinate of the second control point
- * @param x defines the x coordinate of the end point
- * @param y defines the y coordinate of the end point
- */
- bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y) {
- this._currentPath.addBezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y, this._resolution);
- }
- /** Extract holes based on CW / CCW */
- extractHoles() {
- for (const path of this._tempPaths) {
- if (path.area() > 0) {
- this._holes.push(path);
- }
- else {
- this._paths.push(path);
- }
- }
- if (!this._paths.length && this._holes.length) {
- const temp = this._holes;
- this._holes = this._paths;
- this._paths = temp;
- }
- this._tempPaths.length = 0;
- }
- /** Gets the list of paths */
- get paths() {
- return this._paths;
- }
- /** Gets the list of holes */
- get holes() {
- return this._holes;
- }
- }
- // Utility functions
- function CreateShapePath(char, scale, offsetX, offsetY, resolution, fontData) {
- const glyph = fontData.glyphs[char] || fontData.glyphs["?"];
- if (!glyph) {
- // return if there is no glyph data
- return null;
- }
- const shapePath = new ShapePath(resolution);
- if (glyph.o) {
- const outline = glyph.o.split(" ");
- for (let i = 0, l = outline.length; i < l;) {
- const action = outline[i++];
- switch (action) {
- case "m": {
- // moveTo
- const x = parseInt(outline[i++]) * scale + offsetX;
- const y = parseInt(outline[i++]) * scale + offsetY;
- shapePath.moveTo(x, y);
- break;
- }
- case "l": {
- // lineTo
- const x = parseInt(outline[i++]) * scale + offsetX;
- const y = parseInt(outline[i++]) * scale + offsetY;
- shapePath.lineTo(x, y);
- break;
- }
- case "q": {
- // quadraticCurveTo
- const cpx = parseInt(outline[i++]) * scale + offsetX;
- const cpy = parseInt(outline[i++]) * scale + offsetY;
- const cpx1 = parseInt(outline[i++]) * scale + offsetX;
- const cpy1 = parseInt(outline[i++]) * scale + offsetY;
- shapePath.quadraticCurveTo(cpx1, cpy1, cpx, cpy);
- break;
- }
- case "b": {
- // bezierCurveTo
- const cpx = parseInt(outline[i++]) * scale + offsetX;
- const cpy = parseInt(outline[i++]) * scale + offsetY;
- const cpx1 = parseInt(outline[i++]) * scale + offsetX;
- const cpy1 = parseInt(outline[i++]) * scale + offsetY;
- const cpx2 = parseInt(outline[i++]) * scale + offsetX;
- const cpy2 = parseInt(outline[i++]) * scale + offsetY;
- shapePath.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, cpx, cpy);
- break;
- }
- }
- }
- }
- // Extract holes (based on clockwise data)
- shapePath.extractHoles();
- return { offsetX: glyph.ha * scale, shapePath: shapePath };
- }
- /**
- * Creates shape paths from a text and font
- * @param text the text
- * @param size size of the font
- * @param resolution resolution of the font
- * @param fontData defines the font data (can be generated with http://gero3.github.io/facetype.js/)
- * @returns array of ShapePath objects
- */
- export function CreateTextShapePaths(text, size, resolution, fontData) {
- const chars = Array.from(text);
- const scale = size / fontData.resolution;
- const line_height = (fontData.boundingBox.yMax - fontData.boundingBox.yMin + fontData.underlineThickness) * scale;
- const shapePaths = [];
- let offsetX = 0, offsetY = 0;
- for (let i = 0; i < chars.length; i++) {
- const char = chars[i];
- if (char === "\n") {
- offsetX = 0;
- offsetY -= line_height;
- }
- else {
- const ret = CreateShapePath(char, scale, offsetX, offsetY, resolution, fontData);
- if (ret) {
- offsetX += ret.offsetX;
- shapePaths.push(ret.shapePath);
- }
- }
- }
- return shapePaths;
- }
- /**
- * Create a text mesh
- * @param name defines the name of the mesh
- * @param text defines the text to use to build the mesh
- * @param fontData defines the font data (can be generated with http://gero3.github.io/facetype.js/)
- * @param options defines options used to create the mesh
- * @param scene defines the hosting scene
- * @param earcutInjection can be used to inject your own earcut reference
- * @returns a new Mesh
- * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set/text
- */
- export function CreateText(name, text, fontData, options = {
- size: 50,
- resolution: 8,
- depth: 1.0,
- }, scene = null, earcutInjection = earcut) {
- // First we need to generate the paths
- const shapePaths = CreateTextShapePaths(text, options.size || 50, options.resolution || 8, fontData);
- // And extrude them
- const meshes = [];
- let letterIndex = 0;
- for (const shapePath of shapePaths) {
- if (!shapePath.paths.length) {
- continue;
- }
- const holes = shapePath.holes.slice(); // Copy it as we will update the copy
- for (const path of shapePath.paths) {
- const holeVectors = [];
- const shapeVectors = [];
- const points = path.getPoints();
- for (const point of points) {
- shapeVectors.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
- }
- // Holes
- const localHolesCopy = holes.slice();
- for (const hole of localHolesCopy) {
- const points = hole.getPoints();
- let found = false;
- for (const point of points) {
- if (path.isPointInside(point)) {
- found = true;
- break;
- }
- }
- if (!found) {
- continue;
- }
- const holePoints = [];
- for (const point of points) {
- holePoints.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
- }
- holeVectors.push(holePoints);
- // Remove the hole as it was already used
- holes.splice(holes.indexOf(hole), 1);
- }
- // There is at least a hole but it was unaffected
- if (!holeVectors.length && holes.length) {
- for (const hole of holes) {
- const points = hole.getPoints();
- const holePoints = [];
- for (const point of points) {
- holePoints.push(new Vector3(point.x, 0, point.y)); // ExtrudePolygon expects data on the xz plane
- }
- holeVectors.push(holePoints);
- }
- }
- // Extrusion!
- const mesh = ExtrudePolygon(name, {
- shape: shapeVectors,
- holes: holeVectors.length ? holeVectors : undefined,
- depth: options.depth || 1.0,
- faceUV: options.faceUV || options.perLetterFaceUV?.(letterIndex),
- faceColors: options.faceColors || options.perLetterFaceColors?.(letterIndex),
- sideOrientation: Mesh._GetDefaultSideOrientation(options.sideOrientation || Mesh.DOUBLESIDE),
- }, scene, earcutInjection);
- meshes.push(mesh);
- letterIndex++;
- }
- }
- // Then we can merge everyone into one single mesh
- const newMesh = Mesh.MergeMeshes(meshes, true, true);
- if (newMesh) {
- // Move pivot to desired center / bottom / center position
- const bbox = newMesh.getBoundingInfo().boundingBox;
- newMesh.position.x += -(bbox.minimumWorld.x + bbox.maximumWorld.x) / 2; // Mid X
- newMesh.position.y += -(bbox.minimumWorld.y + bbox.maximumWorld.y) / 2; // Mid Z as it will rotate
- newMesh.position.z += -(bbox.minimumWorld.z + bbox.maximumWorld.z) / 2 + bbox.extendSize.z; // Bottom Y as it will rotate
- newMesh.name = name;
- // Rotate 90° Up
- const pivot = new TransformNode("pivot", scene);
- pivot.rotation.x = -Math.PI / 2;
- newMesh.parent = pivot;
- newMesh.bakeCurrentTransformIntoVertices();
- // Remove the pivot
- newMesh.parent = null;
- pivot.dispose();
- }
- return newMesh;
- }
- //# sourceMappingURL=textBuilder.js.map
|