如何在房间元素内创建房间名称文本?

How to create Room name Text inside the room element?

有没有什么方法可以在 forge 查看器的房间元素内创建房间名称文本?

我在 Forge 查看器中有房间元素,如下图所示。 所以,我可以从元素属性中读取房间名称。然后,我想在伪造查看器中创建房间名称文本。可以给我解决方案吗?

提前致谢,

更新2021-06-29

添加了一些条件以避免输入无效数据。

/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM 'AS IS' AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

//ref: 
class TextMeasurer {
    constructor() {
        const SVG_NS = 'http://www.w3.org/2000/svg';

        this.svg = document.createElementNS(SVG_NS, 'svg');

        this.svg.style.visibility = 'hidden';
        this.svg.setAttribute('xmlns', SVG_NS)
        this.svg.setAttribute('width', 0);
        this.svg.setAttribute('height', 0);

        this.svgtext = document.createElementNS(SVG_NS, 'text');
        this.svg.appendChild(this.svgtext);
        this.svgtext.setAttribute('x', 0);
        this.svgtext.setAttribute('y', 0);

        document.querySelector('body').appendChild(this.svg);
    }

    /**
     * Measure a single line of text, including the bounding box, inner size and lead and trail X
     * @param {string} text Single line of text
     * @param {string} fontFamily Name of font family
     * @param {string} fontSize Font size including units
     */
    measureText(text, fontFamily, fontSize) {
        this.svgtext.setAttribute('font-family', fontFamily);
        this.svgtext.setAttribute('font-size', fontSize);
        this.svgtext.textContent = text;

        let bbox = this.svgtext.getBBox();
        let textLength = this.svgtext.getComputedTextLength();

        // measure the overflow before and after the line caused by font side bearing
        // Rendering should start at X + leadX to have the edge of the text appear at X
        // when rendering left-aligned left-to-right
        let baseX = parseInt(this.svgtext.getAttribute('x'));
        let overflow = bbox.width - textLength;
        let leadX = Math.abs(baseX - bbox.x);
        let trailX = overflow - leadX;

        document.querySelector('body').removeChild(this.svg);

        return {
            bbWidth: bbox.width,
            textLength: textLength,
            leadX: leadX,
            trailX: trailX,
            bbHeight: bbox.height
        };
    }
}

class AecRoomTagsExtension extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);

        this.modelBuilder = null;
        this.idPrefix = 100;
    }

    async load() {
        const modelBuilderExt = await this.viewer.loadExtension('Autodesk.Viewing.SceneBuilder');
        const modelBuilder = await modelBuilderExt.addNewModel({
            conserveMemory: false,
            modelNameOverride: 'Room Tags'
        });

        this.modelBuilder = modelBuilder;

        if (!this.viewer.isLoadDone()) {
            this.viewer.addEventListener(
                Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
                () => this.createRoomTags(),
                { once: true }
            );
        } else {
            this.createRoomTags();
        }

        return true;
    }

    unload() {
        this.viewer.impl.unloadModel(this.modelBuilder.model);
        return true;
    }

    pxToMm(val) {
        return val / 3.7795275591;
    }

    mmToFt(val) {
        return val / 304.8;
    }

    createLabel(params) {
        const text = params.text;

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'yellow';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        const fontSize = params.fontSize || 512;
        const fontName = 'serif';
        let offset = 2;
        //Usage:
        let m = new TextMeasurer();
        let textDimensions = m.measureText(text, fontName, `${fontSize}px`);
        canvas.height = textDimensions.bbHeight - (fontSize / 32 + 2) * offset;
        canvas.width = textDimensions.bbWidth + offset + 3 * offset;

        ctx.textBaseline = 'top';
        ctx.fillStyle = '#000';
        ctx.textAlign = 'left';
        ctx.font = `${fontSize}px ${fontName}`;
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, textDimensions.bbWidth + offset * 2, canvas.height);
        ctx.fillStyle = '#000';
        ctx.fillText(text, offset, offset + (fontSize / 32 + 3) * offset);

        ctx.strokeRect(0, 0, textDimensions.bbWidth + offset * 2, canvas.height);
        const labelBlobUrl = canvas.toDataURL();

        //console.log(labelBlobUrl);

        const image = new Image();
        const texture = new THREE.Texture();

        texture.image = image;
        image.src = labelBlobUrl;
        image.onload = function () {
            texture.needsUpdate = true;
        };

        const labelDbId = this.idPrefix++;
        const matName = `label-mat-${labelDbId}`;
        const material = new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide, opacity: 0.8, transparent: true });
        material.map.minFilter = THREE.LinearFilter;
        this.modelBuilder.addMaterial(matName, material);
        const labelMat = this.modelBuilder.findMaterial(matName);

        const planeWidth = this.mmToFt(this.pxToMm(canvas.width));
        const planeHeight = this.mmToFt(this.pxToMm(canvas.height));

        let planeGeo = new THREE.PlaneBufferGeometry(planeWidth, planeHeight);
        let plane = new THREE.Mesh(planeGeo, labelMat);

        plane.matrix = new THREE.Matrix4().compose(
            params.position,
            new THREE.Quaternion(0, 0, 0, 1),
            new THREE.Vector3(1, 1, 1)
        );
        plane.dbId = labelDbId;
        this.modelBuilder.addMesh(plane);
    }

    async createRoomTags() {
        const getRoomDbIdsAsync = () => {
            return new Promise((resolve, reject) => {
                this.viewer.search(
                    'Revit Rooms',
                    (dbIds) => resolve(dbIds),
                    (error) => reject(error),
                    ['Category'],
                    { searchHidden: true }
                );
            });
        };

        const getPropertiesAsync = (dbId, model) => {
            return new Promise((resolve, reject) => {
                model.getProperties2(
                    dbId,
                    (result) => resolve(result),
                    (error) => reject(error)
                );
            });
        };

        const getBoxAsync = (dbId, model) => {
            return new Promise((resolve, reject) => {
                const tree = model.getInstanceTree();
                const frags = model.getFragmentList();

                let bounds = new THREE.Box3();
                tree.enumNodeFragments(dbId, function (fragId) {
                    let box = new THREE.Box3();
                    frags.getWorldBounds(fragId, box);
                    bounds.union(box);
                }, true);
                return resolve(bounds);
            });
        };

        const getRoomNameAsync = async (dbId, model) => {
            const tree = model.getInstanceTree();
            let name = tree.getNodeName(dbId);
            if (!name) {
                const props = await getPropertiesAsync(dbId, model);
                name = props?.name;
            }
            return name;
        };

        try {
            let roomDbIds = await getRoomDbIdsAsync();
            if (!roomDbIds || roomDbIds.length <= 0) {
                throw new Error('No Rooms found in current model');
            }

            const model = this.viewer.model;
            const currentViewableId = this.viewer.model?.getDocumentNode().data.viewableID;
            const masterViews = this.viewer.model?.getDocumentNode().getMasterViews();
            const masterViewIds = masterViews?.map(v => v.data.viewableID);

            if (!masterViewIds.includes(currentViewableId)) {
                throw new Error('Current view does not contain any Rooms');
            }

            for (let i = 0; i < roomDbIds.length; i++) {
                const dbId = roomDbIds[i];

                const name = await getRoomNameAsync(dbId, model);
                if (!name) {
                    console.warn(`[AecRoomTagsExtension]: ${dbId} Room \`${name}\` doesn't have valid name`);
                    continue;
                }

                const roomProps = await getPropertiesAsync(dbId, model);
                const possibleViewableIds = roomProps.properties.filter(prop => prop.attributeName === 'viewable_in').map(prop => prop.displayValue);
                if (!possibleViewableIds.includes(currentViewableId)) {
                    console.warn(`[AecRoomTagsExtension]: ${dbId} Room \`${name}\` is not visible in current view \`${currentViewableId}\``);
                    continue;
                }

                const box = await getBoxAsync(dbId, model);
                if (!box) {
                    console.warn(`[AecRoomTagsExtension]: ${dbId} Room \`${name}\` has an invalid bounding box`);
                    continue;
                }

                const center = box.center();
                if (isNaN(center.x) || isNaN(center.y) || isNaN(center.z)) {
                    console.warn(`[AecRoomTagsExtension]: ${dbId} Room \`${name}\` has an invalid bounding box`);
                    continue;
                }

                //console.log(i, dbId, name, box, center);

                const pos = new THREE.Vector3(
                    center.x,
                    center.y,
                    box.min.z + this.mmToFt(10)
                );

                this.createLabel({
                    text: name.replace(/ *\[[^)]*\] */g, ""),
                    position: pos,
                    fontSize: 512 // in pixel
                });
            }

            // uncomment to prevent selection on tags
            // const dbIds = this.modelBuilder.model.getFragmentList().fragments.fragId2dbId;
            // const model = this.modelBuilder.model;
            // this.viewer.lockSelection(dbIds, true, model);
        } catch (ex) {
            console.warn(`[AecRoomTagsExtension]: ${ex}`);
        }
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension('Autodesk.ADN.AecRoomTagsExtension', AecRoomTagsExtension);

===============================

类似于 Gird 解决方案:

不完美,但它有效。您可能需要根据您的模型调整标签放置点(位置)。目前,标签放置在房间边界框底面的中心。

/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM 'AS IS' AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

//ref: 
class TextMeasurer {
    constructor() {
        const SVG_NS = 'http://www.w3.org/2000/svg';

        this.svg = document.createElementNS(SVG_NS, 'svg');

        this.svg.style.visibility = 'hidden';
        this.svg.setAttribute('xmlns', SVG_NS)
        this.svg.setAttribute('width', 0);
        this.svg.setAttribute('height', 0);

        this.svgtext = document.createElementNS(SVG_NS, 'text');
        this.svg.appendChild(this.svgtext);
        this.svgtext.setAttribute('x', 0);
        this.svgtext.setAttribute('y', 0);

        document.querySelector('body').appendChild(this.svg);
    }

    /**
     * Measure a single line of text, including the bounding box, inner size and lead and trail X
     * @param {string} text Single line of text
     * @param {string} fontFamily Name of font family
     * @param {string} fontSize Font size including units
     */
    measureText(text, fontFamily, fontSize) {
        this.svgtext.setAttribute('font-family', fontFamily);
        this.svgtext.setAttribute('font-size', fontSize);
        this.svgtext.textContent = text;

        let bbox = this.svgtext.getBBox();
        let textLength = this.svgtext.getComputedTextLength();

        // measure the overflow before and after the line caused by font side bearing
        // Rendering should start at X + leadX to have the edge of the text appear at X
        // when rendering left-aligned left-to-right
        let baseX = parseInt(this.svgtext.getAttribute('x'));
        let overflow = bbox.width - textLength;
        let leadX = Math.abs(baseX - bbox.x);
        let trailX = overflow - leadX;

        document.querySelector('body').removeChild(this.svg);

        return {
            bbWidth: bbox.width,
            textLength: textLength,
            leadX: leadX,
            trailX: trailX,
            bbHeight: bbox.height
        };
    }
}

class AecRoomTagsExtension extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);

        this.modelBuilder = null;
        this.idPrefix = 100;
    }

    async load() {
        const modelBuilderExt = await this.viewer.loadExtension('Autodesk.Viewing.SceneBuilder');
        const modelBuilder = await modelBuilderExt.addNewModel({
            conserveMemory: false,
            modelNameOverride: 'Room Tags'
        });

        this.modelBuilder = modelBuilder;

        if (!this.viewer.isLoadDone()) {
            this.viewer.addEventListener(
                Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
                () => this.createRoomTags(),
                { once: true }
            );
        } else {
            this.createRoomTags();
        }

        return true;
    }

    unload() {
        this.viewer.impl.unloadModel(this.modelBuilder.model);
        return true;
    }

    pxToMm(val) {
        return val / 3.7795275591;
    }

    mmToFt(val) {
        return val / 304.8;
    }

    createLabel(params) {
        const text = params.text;

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'yellow';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        const fontSize = params.fontSize || 512;
        const fontName = 'serif';
        let offset = 2;
        //Usage:
        let m = new TextMeasurer();
        let textDimensions = m.measureText(text, fontName, `${fontSize}px`);
        canvas.height = textDimensions.bbHeight  - (fontSize / 32 + 2) * offset;
        canvas.width = textDimensions.bbWidth + offset + 3 * offset;

        ctx.textBaseline = 'top';
        ctx.fillStyle = '#000';
        ctx.textAlign = 'left';
        ctx.font = `${fontSize}px ${fontName}`;
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, textDimensions.bbWidth + offset * 2, canvas.height);
        ctx.fillStyle = '#000';
        ctx.fillText(text, offset, offset + (fontSize / 32 + 3) * offset);

        ctx.strokeRect(0, 0, textDimensions.bbWidth + offset * 2, canvas.height);
        const labelBlobUrl = canvas.toDataURL();

        //console.log(labelBlobUrl);

        const image = new Image();
        const texture = new THREE.Texture();

        texture.image = image;
        image.src = labelBlobUrl;
        image.onload = function () {
            texture.needsUpdate = true;
        };

        const planeWidth = this.mmToFt(this.pxToMm(canvas.width));
        const planeHeight = this.mmToFt(this.pxToMm(canvas.height));

        let planeGeo = new THREE.PlaneBufferGeometry(planeWidth, planeHeight);
        let plane = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide, opacity: 0.8, transparent: true }));

        plane.matrix = new THREE.Matrix4().compose(
            params.position,
            new THREE.Quaternion(0, 0, 0, 1),
            new THREE.Vector3(1, 1, 1)
        );
        plane.dbId = this.idPrefix++;
        this.modelBuilder.addMesh(plane);
    }

    async createRoomTags() {
        const getRoomDbIdsAsync = () => {
            return new Promise((resolve, reject) => {
                this.viewer.search(
                    'Revit Rooms',
                    (dbIds) => resolve(dbIds),
                    (error) => reject(error),
                    ['Category'],
                    { searchHidden: true }
                );
            });
        };

        const getPropertiesAsync = (dbId, model) => {
            return new Promise((resolve, reject) => {
                model.getProperties2(
                    dbId,
                    (result) => resolve(result),
                    (error) => reject(error)
                );
            });
        };

        const getBoxAsync = (dbId, model) => {
            return new Promise((resolve, reject) => {
                const tree = model.getInstanceTree();
                const frags = model.getFragmentList();
                tree.enumNodeFragments(dbId, function (fragId) {
                    let bounds = new THREE.Box3();
                    frags.getWorldBounds(fragId, bounds);
                    return resolve(bounds);
                }, true);
            });
        };

        const getRoomName = (dbId, model) => {
            const tree = model.getInstanceTree();
            return tree.getNodeName(dbId);
        };

        try {
            const roomDbIds = await getRoomDbIdsAsync();
            if (!roomDbIds || roomDbIds.length <= 0) {
                throw new Error('No Rooms found in current model');
            }

            const model = this.viewer.model;
            const currentViewableId = this.viewer.model?.getDocumentNode().data.viewableID;
            const firstRoomProps = await getPropertiesAsync(roomDbIds[0], this.viewer.model);
            const possibleViewableIds = firstRoomProps.properties.filter(prop => prop.attributeName === 'viewable_in').map(prop => prop.displayValue);
            const masterViews = this.viewer.model?.getDocumentNode().getMasterViews();
            const masterViewIds = masterViews?.map(v => v.data.viewableID);

            if (!masterViewIds.includes(currentViewableId) || !possibleViewableIds.includes(currentViewableId)) {
                throw new Error('Current view does not contain any Rooms');
            }

            for (let i = roomDbIds.length - 1; i >= 0; i--) {
                const dbId = roomDbIds[i];
                const box = await getBoxAsync(dbId, model);
                const name = getRoomName(dbId, model);
                const center = box.center();
                const pos = new THREE.Vector3(
                    center.x,
                    center.y,
                    box.min.z + this.mmToFt(10)
                );

                this.createLabel({
                    text: name.replace(/ *\[[^)]*\] */g, ""),
                    position: pos,
                    fontSize: 512 // in pixel
                });
            }

            // uncomment to prevent selection on tags
            // const dbIds = this.modelBuilder.model.getFragmentList().fragments.fragId2dbId;
            // const model = this.modelBuilder.model;
            // this.viewer.lockSelection(dbIds, true, model);
        } catch (ex) {
            console.warn(`[AecRoomTagsExtension]: ${ex}`);
        }
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension('Autodesk.ADN.AecRoomTagsExtension', AecRoomTagsExtension);

这里是 dmeo 快照: