dimensions

 avatar
unknown
plain_text
a month ago
26 kB
3
Indexable
import * as BABYLON from 'babylonjs';
import { AdvancedDynamicTexture, Rectangle, Control, TextBlock } from 'babylonjs-gui';
import { Equipment } from '../models/entities/equipment';
import { MeshEnum } from '../models/enums/mesh-enum';
import { EquipmentNodeInformation } from '../models/entities/equipment-node-information';
import { DimensionPartEnum } from '../models/enums/dimension-part-enum';
import { DimensionTypeEnum } from '../models/enums/dimension-type-enum';
import { DimensionDirectionEnum } from '../models/enums/dimension-direction-enum';

interface DimensionTextCache {
    mesh: BABYLON.Mesh;
    texture: AdvancedDynamicTexture;
    textBlock: TextBlock;
}

interface DimensionMeshSet {
    mainLine: BABYLON.Mesh;
    leftCorner: BABYLON.Mesh;
    rightCorner: BABYLON.Mesh;
}

export class DimensionsRenderer {
    private readonly DIMENSION_CORNER_SIZE = { thickness: 0.1, length: 1.5 };
    private readonly DIMENSION_LINE_SIZE = { thickness: 0.1 };
    private readonly DIMENSION_COLOR = new BABYLON.Color3(89 / 255, 88 / 255, 88 / 255);
    private readonly RAY_LENGTH = 100;

    private scene: BABYLON.Scene;
    private scale: number;
    private dimensionMaterial: BABYLON.StandardMaterial;

    private dimensionNodes: Map<DimensionDirectionEnum, BABYLON.TransformNode> = new Map();
    private dimensionMeshes: Map<string, DimensionMeshSet> = new Map();
    private dimensionTexts: Map<string, DimensionTextCache> = new Map();

    private readonly rayDirections = {
        back: new BABYLON.Vector3(0, 0, -1),
        right: new BABYLON.Vector3(1, 0, 0),
        forward: new BABYLON.Vector3(0, 0, 1),
        left: new BABYLON.Vector3(-1, 0, 0),
    };

    constructor(scene: BABYLON.Scene, scale: number) {
        this.scene = scene;
        this.scale = scale;

        this.dimensionMaterial = new BABYLON.StandardMaterial('dimension-material', this.scene);
        this.dimensionMaterial.diffuseColor = this.DIMENSION_COLOR;
        this.dimensionMaterial.emissiveColor = this.DIMENSION_COLOR;
        this.dimensionMaterial.disableLighting = true;
    }

    disposeDimensions(): void {
        this.dimensionTexts.forEach((cache) => {
            if (cache.texture) {
                cache.texture.dispose();
            }
            if (cache.mesh) {
                cache.mesh.dispose();
            }
        });
        this.dimensionTexts.clear();

        this.dimensionMeshes.forEach((meshSet) => {
            if (meshSet.mainLine) meshSet.mainLine.dispose();
            if (meshSet.leftCorner) meshSet.leftCorner.dispose();
            if (meshSet.rightCorner) meshSet.rightCorner.dispose();
        });
        this.dimensionMeshes.clear();

        this.dimensionNodes.forEach((node) => {
            if (node) {
                node.dispose();
            }
        });
        this.dimensionNodes.clear();

        Object.values(DimensionDirectionEnum).forEach((direction) => {
            const transformNode = this.scene.getTransformNodeByName(
                `${direction}-${MeshEnum.DIMENSION}-${DimensionPartEnum.PARENT_NODE}`,
            );
            if (transformNode) {
                const children = transformNode.getChildMeshes();
                children.forEach((child) => {
                    child.dispose();
                });
                transformNode.dispose();
            }
        });

        const textBoxes = this.scene.getMeshesByID(MeshEnum.DIMENSION_TEXT);
        textBoxes.forEach((textBox: BABYLON.AbstractMesh) => {
            textBox.dispose();
        });

        Object.values(DimensionTypeEnum).forEach((dimensionType) => {
            const dimensionLines = this.scene.getMeshesByID(
                `${dimensionType}-${MeshEnum.DIMENSION}-${DimensionPartEnum.MAIN_LINE}`,
            );
            const leftCorners = this.scene.getMeshesByID(
                `${dimensionType}-${MeshEnum.DIMENSION}-${DimensionPartEnum.LEFT_CORNER}`,
            );
            const rightCorners = this.scene.getMeshesByID(
                `${dimensionType}-${MeshEnum.DIMENSION}-${DimensionPartEnum.RIGHT_CORNER}`,
            );

            [...dimensionLines, ...leftCorners, ...rightCorners].forEach((mesh) => {
                if (mesh) mesh.dispose();
            });
        });
    }

    getTransformNodeSize(equipmentNodeInformation: EquipmentNodeInformation): BABYLON.Vector3 {
        const width = equipmentNodeInformation.width / this.scale;
        const length = equipmentNodeInformation.length / this.scale;
        const height = equipmentNodeInformation.height / this.scale;

        const rotationRad = BABYLON.Tools.ToRadians(equipmentNodeInformation.rotation);
        const cosTheta = Math.abs(Math.cos(rotationRad));
        const sinTheta = Math.abs(Math.sin(rotationRad));

        // calculate rotated dimensions
        const size = new BABYLON.Vector3(
            width * cosTheta + length * sinTheta,
            height,
            width * sinTheta + length * cosTheta,
        );

        return size;
    }

    drawDimensions(tn: BABYLON.TransformNode, equipment: Equipment): void {
        this.scene.blockMaterialDirtyMechanism = true;
        // force update of transform node and its children
        tn.computeWorldMatrix(true);
        tn.getChildMeshes().forEach((mesh) => mesh.computeWorldMatrix(true));

        const size = this.getTransformNodeSize(equipment.nodeInformation);

        const elevation = equipment.nodeInformation.elevation / this.scale;
        const height = equipment.nodeInformation.height / this.scale;

        const tnPosition = tn.position.clone();

        // cast rays in all four directions to find the nearest walls
        const dimensionResults = this.castRaysForDimensions(tnPosition, size, height);

        if (dimensionResults.back.distance > 0.1) {
            this.updateOrCreateDimension(
                DimensionDirectionEnum.BACK,
                dimensionResults.back.position,
                { width: 0, height: 0, length: dimensionResults.back.distance },
                DimensionTypeEnum.LENGTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.BACK);
        }

        if (dimensionResults.right.distance > 0.1) {
            this.updateOrCreateDimension(
                DimensionDirectionEnum.RIGHT,
                dimensionResults.right.position,
                { width: dimensionResults.right.distance, height: 0, length: 0 },
                DimensionTypeEnum.WIDTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.RIGHT);
        }

        if (dimensionResults.forward.distance > 0.1) {
            this.updateOrCreateDimension(
                DimensionDirectionEnum.FORWARD,
                dimensionResults.forward.position,
                { width: 0, height: 0, length: dimensionResults.forward.distance },
                DimensionTypeEnum.LENGTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.FORWARD);
        }

        if (dimensionResults.left.distance > 0.1) {
            this.updateOrCreateDimension(
                DimensionDirectionEnum.LEFT,
                dimensionResults.left.position,
                { width: dimensionResults.left.distance, height: 0, length: 0 },
                DimensionTypeEnum.WIDTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.LEFT);
        }

        // ground dimension
        if (elevation > 0.1) {
            const startGround = tnPosition.clone();
            startGround.y += elevation;

            this.updateOrCreateDimension(
                DimensionDirectionEnum.GROUND,
                startGround,
                { width: 0, height: elevation, length: 0 },
                DimensionTypeEnum.HEIGHT,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.GROUND);
        }

        // Restore material dirty mechanism after batch operations
        this.scene.blockMaterialDirtyMechanism = false;
    }

    private hideDimension(direction: DimensionDirectionEnum): void {
        const transformNode = this.dimensionNodes.get(direction);
        if (transformNode) {
            transformNode.setEnabled(false);
        }
    }

    private castRaysForDimensions(
        position: BABYLON.Vector3,
        size: BABYLON.Vector3,
        height: number,
    ): {
        back: { distance: number; position: BABYLON.Vector3 };
        right: { distance: number; position: BABYLON.Vector3 };
        forward: { distance: number; position: BABYLON.Vector3 };
        left: { distance: number; position: BABYLON.Vector3 };
    } {
        // create predicate to exclude non-wall meshes
        const predicate = (mesh: BABYLON.AbstractMesh) => {
            return mesh.id === MeshEnum.WALL || mesh.id === MeshEnum.INNER_WALL;
        };

        const startBack = position.clone();
        startBack.z -= size.z / 2;
        startBack.y = position.y - height / 2;

        const startRight = position.clone();
        startRight.x += size.x / 2;
        startRight.y = position.y - height / 2;

        const startForward = position.clone();
        startForward.z += size.z / 2;
        startForward.y = position.y - height / 2;

        const startLeft = position.clone();
        startLeft.x -= size.x / 2;
        startLeft.y = position.y - height / 2;

        const backRay = new BABYLON.Ray(startBack, new BABYLON.Vector3(0, 0, -1), this.RAY_LENGTH);
        const rightRay = new BABYLON.Ray(startRight, new BABYLON.Vector3(1, 0, 0), this.RAY_LENGTH);
        const forwardRay = new BABYLON.Ray(startForward, new BABYLON.Vector3(0, 0, 1), this.RAY_LENGTH);
        const leftRay = new BABYLON.Ray(startLeft, new BABYLON.Vector3(-1, 0, 0), this.RAY_LENGTH);

        const backHit = this.scene.pickWithRay(backRay, predicate);
        const rightHit = this.scene.pickWithRay(rightRay, predicate);
        const forwardHit = this.scene.pickWithRay(forwardRay, predicate);
        const leftHit = this.scene.pickWithRay(leftRay, predicate);

        // calculate dimensions based on ray hits
        const backDistance = backHit && backHit.hit ? backHit.distance : 0;
        const rightDistance = rightHit && rightHit.hit ? rightHit.distance : 0;
        const forwardDistance = forwardHit && forwardHit.hit ? forwardHit.distance : 0;
        const leftDistance = leftHit && leftHit.hit ? leftHit.distance : 0;

        // calculate dimension positions
        const backPosition = startBack.clone();
        if (backDistance > 0) {
            backPosition.z -= backDistance / 2;
        }

        const rightPosition = startRight.clone();
        if (rightDistance > 0) {
            rightPosition.x += rightDistance / 2;
        }

        const forwardPosition = startForward.clone();
        if (forwardDistance > 0) {
            forwardPosition.z += forwardDistance / 2;
        }

        const leftPosition = startLeft.clone();
        if (leftDistance > 0) {
            leftPosition.x -= leftDistance / 2;
        }

        return {
            back: { distance: backDistance, position: backPosition },
            right: { distance: rightDistance, position: rightPosition },
            forward: { distance: forwardDistance, position: forwardPosition },
            left: { distance: leftDistance, position: leftPosition },
        };
    }

    private updateOrCreateDimension(
        direction: DimensionDirectionEnum,
        position: BABYLON.Vector3,
        dimensions: { width: number; height: number; length: number; scale?: BABYLON.Vector3 },
        type: DimensionTypeEnum,
    ): void {
        const transformNodeName = `${direction}-${MeshEnum.DIMENSION}-${DimensionPartEnum.PARENT_NODE}`;
        let transformNode = this.dimensionNodes.get(direction);

        if (!transformNode) {
            transformNode =
                this.scene.getTransformNodeByName(transformNodeName) ||
                new BABYLON.TransformNode(transformNodeName, this.scene);
            transformNode.id = transformNodeName;
            this.dimensionNodes.set(direction, transformNode);
        }

        transformNode.setEnabled(true);

        let text: number = 0;
        let dimensionSize: { width: number; height: number; depth: number } = { width: 0, height: 0, depth: 0 };
        let cornerSize: { width: number; height: number; depth: number } = { width: 0, height: 0, depth: 0 };

        let textPosition = new BABYLON.Vector3();
        let dimensionLeftCornerPosition = new BABYLON.Vector3();
        let dimensionRightCornerPosition = new BABYLON.Vector3();

        switch (type) {
            case DimensionTypeEnum.WIDTH:
                dimensionSize = {
                    width: 1, // for scaling
                    height: this.DIMENSION_LINE_SIZE.thickness,
                    depth: this.DIMENSION_CORNER_SIZE.thickness,
                };
                dimensionLeftCornerPosition = new BABYLON.Vector3(
                    position.x - dimensions.width / 2,
                    position.y,
                    position.z,
                );
                dimensionRightCornerPosition = new BABYLON.Vector3(
                    position.x + dimensions.width / 2,
                    position.y,
                    position.z,
                );

                cornerSize = {
                    width: this.DIMENSION_CORNER_SIZE.thickness,
                    height: this.DIMENSION_CORNER_SIZE.length,
                    depth: this.DIMENSION_CORNER_SIZE.thickness,
                };
                textPosition = new BABYLON.Vector3(position.x, position.y, position.z);
                text = dimensions.width;
                break;
            case DimensionTypeEnum.LENGTH:
                dimensionLeftCornerPosition = new BABYLON.Vector3(
                    position.x,
                    position.y,
                    position.z - dimensions.length / 2,
                );
                dimensionRightCornerPosition = new BABYLON.Vector3(
                    position.x,
                    position.y,
                    position.z + dimensions.length / 2,
                );
                dimensionSize = {
                    width: this.DIMENSION_CORNER_SIZE.thickness,
                    height: this.DIMENSION_LINE_SIZE.thickness,
                    depth: 1,
                };
                cornerSize = {
                    width: this.DIMENSION_CORNER_SIZE.thickness,
                    height: this.DIMENSION_CORNER_SIZE.length,
                    depth: this.DIMENSION_CORNER_SIZE.thickness,
                };

                textPosition = new BABYLON.Vector3(position.x, position.y, position.z);
                text = dimensions.length;
                break;
            case DimensionTypeEnum.HEIGHT:
                const lineHeight = dimensions.height;

                const linePosition = position.clone();
                linePosition.y = position.y - lineHeight / 2;

                dimensionLeftCornerPosition = new BABYLON.Vector3(position.x, position.y - lineHeight, position.z);
                dimensionRightCornerPosition = new BABYLON.Vector3(position.x, position.y, position.z);

                dimensionSize = {
                    width: this.DIMENSION_CORNER_SIZE.thickness,
                    height: 1,
                    depth: this.DIMENSION_CORNER_SIZE.thickness,
                };

                cornerSize = {
                    width: this.DIMENSION_CORNER_SIZE.length,
                    height: this.DIMENSION_CORNER_SIZE.thickness,
                    depth: this.DIMENSION_CORNER_SIZE.thickness,
                };

                textPosition = new BABYLON.Vector3(position.x + 1, position.y - lineHeight / 2, position.z);

                text = dimensions.height;

                position = linePosition;
                break;
        }

        if (text <= 0) {
            return;
        }

        const leftCornerName = `${type}-${MeshEnum.DIMENSION}-${DimensionPartEnum.LEFT_CORNER}-${direction}`;
        const rightCornerName = `${type}-${MeshEnum.DIMENSION}-${DimensionPartEnum.RIGHT_CORNER}-${direction}`;
        const mainLineName = `${type}-${MeshEnum.DIMENSION}-${DimensionPartEnum.MAIN_LINE}-${direction}`;
        const meshSetKey = `${type}-${MeshEnum.DIMENSION}-${direction}`;

        let meshSet = this.dimensionMeshes.get(meshSetKey);

        if (!meshSet) {
            const mainLine = BABYLON.MeshBuilder.CreateBox(mainLineName, dimensionSize, this.scene);
            const leftCorner = BABYLON.MeshBuilder.CreateBox(leftCornerName, cornerSize, this.scene);
            const rightCorner = BABYLON.MeshBuilder.CreateBox(rightCornerName, cornerSize, this.scene);

            [mainLine, leftCorner, rightCorner].forEach((mesh) => {
                if (mesh) {
                    mesh.material = this.dimensionMaterial;
                    mesh.isPickable = false;
                    mesh.parent = transformNode;
                }
            });

            mainLine.id = mainLineName;
            leftCorner.id = leftCornerName;
            rightCorner.id = rightCornerName;

            // initial scaling based on dimension type
            switch (type) {
                case DimensionTypeEnum.WIDTH:
                    mainLine.scaling.x = dimensions.width;
                    break;
                case DimensionTypeEnum.LENGTH:
                    mainLine.scaling.z = dimensions.length;
                    break;
                case DimensionTypeEnum.HEIGHT:
                    mainLine.scaling.y = dimensions.height;
                    break;
            }

            meshSet = {
                mainLine,
                leftCorner,
                rightCorner,
            };

            this.dimensionMeshes.set(meshSetKey, meshSet);
        } else {
            if (meshSet.mainLine) {
                switch (type) {
                    case DimensionTypeEnum.WIDTH:
                        meshSet.mainLine.scaling.x = dimensions.width;
                        break;
                    case DimensionTypeEnum.LENGTH:
                        meshSet.mainLine.scaling.z = dimensions.length;
                        break;
                    case DimensionTypeEnum.HEIGHT:
                        meshSet.mainLine.scaling.y = dimensions.height;
                        break;
                }
            }
        }

        meshSet.mainLine.position = position.clone();
        meshSet.leftCorner.position = dimensionLeftCornerPosition.clone();
        meshSet.rightCorner.position = dimensionRightCornerPosition.clone();

        this.updateOrCreateDimensionText(meshSet.mainLine, Math.round(text * 100), textPosition);
    }

    private updateOrCreateDimensionText(mesh: BABYLON.Mesh, inputText: number, position?: BABYLON.Vector3): void {
        const textBoxName = `${MeshEnum.DIMENSION_TEXT}-${mesh.name}`;
        let textCache = this.dimensionTexts.get(textBoxName);

        if (!textCache) {
            const textBox = BABYLON.MeshBuilder.CreatePlane(textBoxName, { width: 3.5, height: 1.25 }, this.scene);
            textBox.id = MeshEnum.DIMENSION_TEXT;
            textBox.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
            textBox.renderingGroupId = 1;

            const label = new Rectangle(`label for ${mesh.name}`);
            label.background = 'black';
            label.height = '100%';
            label.width = '100%';
            label.cornerRadius = 4;
            label.thickness = 0;
            label.useBitmapCache = true;

            const input = new TextBlock('text-input');
            input.width = '100%';
            input.height = '100%';
            input.text = inputText.toString();
            input.color = 'white';
            input.fontFamily = 'Poppins';
            input.fontSize = '70%';
            input.fontWeight = 'bold';
            input.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
            input.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;

            const advancedTexture = AdvancedDynamicTexture.CreateForMesh(textBox, 96, 32, false, undefined, !position);
            advancedTexture.name = `${mesh.name}-textbox-texture`;
            advancedTexture.addControl(label);
            label.addControl(input);

            textCache = {
                mesh: textBox,
                texture: advancedTexture,
                textBlock: input,
            };
            this.dimensionTexts.set(textBoxName, textCache);
        } else {
            textCache.textBlock.text = inputText.toString();
            textCache.mesh.setEnabled(true);
        }

        if (position) {
            textCache.mesh.position.copyFrom(position);
        } else {
            textCache.mesh.position.copyFrom(mesh.position);
        }
    }

    updateDimensionsPositions(tn: BABYLON.TransformNode, equipment: Equipment): void {
        this.scene.blockMaterialDirtyMechanism = true;

        const size = this.getTransformNodeSize(equipment.nodeInformation);
        const elevation = equipment.nodeInformation.elevation / this.scale;
        const height = equipment.nodeInformation.height / this.scale;
        const tnPosition = tn.position;

        const dimensionResults = this.castRaysForDimensions(tnPosition, size, height);

        if (dimensionResults.back.distance > 0.1) {
            this.updateDimensionPosition(
                DimensionDirectionEnum.BACK,
                dimensionResults.back.position,
                dimensionResults.back.distance,
                DimensionTypeEnum.LENGTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.BACK);
        }

        if (dimensionResults.right.distance > 0.1) {
            this.updateDimensionPosition(
                DimensionDirectionEnum.RIGHT,
                dimensionResults.right.position,
                dimensionResults.right.distance,
                DimensionTypeEnum.WIDTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.RIGHT);
        }

        if (dimensionResults.forward.distance > 0.1) {
            this.updateDimensionPosition(
                DimensionDirectionEnum.FORWARD,
                dimensionResults.forward.position,
                dimensionResults.forward.distance,
                DimensionTypeEnum.LENGTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.FORWARD);
        }

        if (dimensionResults.left.distance > 0.1) {
            this.updateDimensionPosition(
                DimensionDirectionEnum.LEFT,
                dimensionResults.left.position,
                dimensionResults.left.distance,
                DimensionTypeEnum.WIDTH,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.LEFT);
        }

        if (elevation > 0.1) {
            const startGround = tnPosition.clone();
            startGround.y += elevation;
            this.updateDimensionPosition(
                DimensionDirectionEnum.GROUND,
                startGround,
                elevation,
                DimensionTypeEnum.HEIGHT,
            );
        } else {
            this.hideDimension(DimensionDirectionEnum.GROUND);
        }

        this.scene.blockMaterialDirtyMechanism = false;
    }

    private updateDimensionPosition(
        direction: DimensionDirectionEnum,
        position: BABYLON.Vector3,
        distance: number,
        type: DimensionTypeEnum,
    ): void {
        const transformNode = this.dimensionNodes.get(direction);
        if (!transformNode) return;

        transformNode.setEnabled(true);

        const meshSetKey = `${type}-${MeshEnum.DIMENSION}-${direction}`;
        const meshSet = this.dimensionMeshes.get(meshSetKey);
        if (!meshSet) return;

        // update positions and scaling based on dimension type
        switch (type) {
            case DimensionTypeEnum.WIDTH:
                meshSet.mainLine.position.copyFromFloats(position.x, position.y, position.z);
                meshSet.leftCorner.position.copyFromFloats(position.x - distance / 2, position.y, position.z);
                meshSet.rightCorner.position.copyFromFloats(position.x + distance / 2, position.y, position.z);
                meshSet.mainLine.scaling.x = distance;
                break;
            case DimensionTypeEnum.LENGTH:
                meshSet.mainLine.position.copyFromFloats(position.x, position.y, position.z);
                meshSet.leftCorner.position.copyFromFloats(position.x, position.y, position.z - distance / 2);
                meshSet.rightCorner.position.copyFromFloats(position.x, position.y, position.z + distance / 2);
                meshSet.mainLine.scaling.z = distance;
                break;
            case DimensionTypeEnum.HEIGHT:
                meshSet.mainLine.position.copyFromFloats(position.x, position.y - distance / 2, position.z);
                meshSet.leftCorner.position.copyFromFloats(position.x, position.y - distance, position.z);
                meshSet.rightCorner.position.copyFromFloats(position.x, position.y, position.z);
                meshSet.mainLine.scaling.y = distance;
                break;
        }

        const textCache = this.dimensionTexts.get(`${MeshEnum.DIMENSION_TEXT}-${meshSet.mainLine.name}`);
        if (textCache) {
            textCache.textBlock.text = Math.round(distance * 100).toString();
            if (type === DimensionTypeEnum.HEIGHT) {
                textCache.mesh.position.copyFromFloats(position.x + 1, position.y - distance / 2, position.z);
            } else {
                textCache.mesh.position.copyFromFloats(position.x, position.y, position.z);
            }
        }
    }
}
Editor is loading...
Leave a Comment