import {AbstractMesh, Color3, DynamicTexture, Material, MeshBuilder, StandardMaterial, TransformNode} from "@babylonjs/core"; import log from "loglevel"; const textLogger: log.Logger = log.getLogger('TextLabel'); export function updateTextNode(mesh: AbstractMesh, text: string) { if (!mesh) { textLogger.error("updateTextNode: mesh is null"); return null; } const textNodes = mesh.getChildren((node) => { return node.metadata?.label == true; }); if (textNodes && textNodes.length > 0) { textNodes.forEach((node) => { node.parent = null; node.dispose(false, true); }); } if (!text) { return null; } //Set font const height = 0.08; const font_size = 24; const font = "bold " + font_size + "px Arial"; //Set height for dynamic texture const DTHeight = 1.5 * font_size; //or set as wished //Calc Ratio const ratio = height / DTHeight; //Use a temporary dynamic texture to calculate the length of the text on the dynamic texture canvas const temp = new DynamicTexture("DynamicTexture", 32, mesh.getScene()); const tmpctx = temp.getContext(); tmpctx.font = font; const DTWidth = tmpctx.measureText(text).width + 8; //Calculate width the plane has to be const planeWidth = DTWidth * ratio; temp.dispose(); //Create dynamic texture and write the text const dynamicTexture = new DynamicTexture("DynamicTexture", { width: DTWidth, height: DTHeight }, mesh.getScene(), false); dynamicTexture.drawText(text, null, null, font, "#ffffff", "#000000", true); const mat = new StandardMaterial("mat", mesh.getScene()); mat.metadata = { isUI: true }; // Mark as UI to prevent rendering mode modifications mat.diffuseColor = Color3.Black(); mat.disableLighting = true; mat.backFaceCulling = false; mat.emissiveTexture = dynamicTexture; mat.freeze(); const plane1 = createPlane(mat, mesh, text, planeWidth, height); //const plane2 = createPlane(mat, mesh, text, planeWidth, height); //plane2.rotation.y = Math.PI; } function createPlane(mat: Material, mesh: AbstractMesh, text: string, planeWidth: number, height: number): AbstractMesh { const plane = MeshBuilder.CreatePlane("text" + text, {width: planeWidth, height: height}, mesh.getScene()); plane.parent = mesh; plane.scaling.y = (1 / mesh.scaling.y); plane.scaling.x = (1 / mesh.scaling.x); plane.scaling.z = (1 / mesh.scaling.z); plane.material = mat; plane.metadata = {exportable: true, label: true}; if (mesh.metadata?.template == "#connection-template") { plane.billboardMode = AbstractMesh.BILLBOARDMODE_Y; plane.position.y = mesh.position.y + .1; } else { // Calculate label position using world space bounding box // This ensures labels are positioned correctly regardless of mesh transforms mesh.computeWorldMatrix(true); mesh.refreshBoundingInfo({}); // Get the top of the bounding box in world space const top = mesh.getBoundingInfo().boundingBox.maximumWorld; // Convert world space position to mesh's local space // Use temporary TransformNode to handle the transformation const temp = new TransformNode("temp", mesh.getScene()); temp.position = top; temp.setParent(mesh); const y = temp.position.y; temp.dispose(); // Position label above the mesh with offset // Add additional offset for the scaled height of the label plane.position.y = y + 0.06 + (height * plane.scaling.y / 2); } plane.addLODLevel(3, null); return plane; }