immersive2/src/util/functions/updateTextNode.ts
Michael Mainguy 0b81605bdf Fix label positioning to use world space bounding boxes
Labels were incorrectly mixing local and global transform matrices, causing
incorrect positioning on scaled/rotated meshes. Now properly converts world
space bounding box positions to mesh local space using temporary TransformNode.

Changes:
- updateTextNode.ts: Use boundingBox.maximumWorld instead of boundingSphere.maximum
- diagramObject.ts: Add empty object param to refreshBoundingInfo()
- inputTextView.ts: Adjust input handle default position and mesh offsets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:52:04 -06:00

102 lines
3.6 KiB
TypeScript

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;
}