All about our software developer journey

Month: April 2023

BIM World 2023

Every year, BIM World event is kind of unmissable. 2h hours of train make this travel pretty easy – even with the social movements of the day, which did not affect us by the way.

Since 2019, BIM World is held at Paris Expo – Porte de Versailles which is the biggest exhibiting centre in France and recognizable by his central ring of screens.

After a complete tour of the show to see most of the exhibitors, we went to the Autodesk booth. We were happy to discuss with some “Autodeskers” about APS and products news.

We attended some great presentations around the show.

After this first day, we were invited to the “SoirĂ©e de l’Excellence” organized by Autodesk. It took place in a beautiful room near the BIMWorld, where we had a fantastic view on Paris at night.

We attended to four talks. Starting with the future of technologies in AEC, where we had a great preview of the way AI can be more integrated into current software. Followed by two different cases about train tracks construction and train station building.

Then, for the second time of the day we followed the vision of XR presentation, from the current VR products to the new developments on their roadmap.

We spent our second day discussing with some acquaintances and customers. As usual, for us the greatest value added of this event is to strengthen relationships with people we already know or have not met in person.

The focus was made on digital twins and sustainability and we were a little disappointed about the lack of great innovation of some exhibitors on these subjects compared to the previous years.

But at least some of them are trying to attract people on their booth with childhood references !

This week, there is the Laval Virtual event which is dedicated to VR / AR. I will be there on Thursday and I will give you my thought about it.

Make measure calibrations persistent in APS Viewer

The Autodesk Platform Service – APS (formerly Forge) Viewer has a built-in extension to take measures of 2D and 3D models.

Sometimes, you can have a model or a sheet with inconsistent units ending up with wrong measure values. So a calibration tool is allowing you to set the right length between two points. It defines a scaling factor applied to all measures.

How to make it persistent?

Here is some tips to get the current calibration, save it and then set it when the viewer loads.

Access to the measure extension.

For that, we can wait for the EXTENSION_LOADED_EVENT and check if the extension id is Autodesk.Measure to be sure that the extension is loaded.

viewer.addEventListener(Autodesk.Viewing.EXTENSION_LOADED_EVENT, function(event){
  if(event.extensionId === 'Autodesk.Measure')
  {
    //Now we are sure that the extension is loaded
  }
});

Now we can get the extension.

const measureExtension = viewer.getExtension('Autodesk.Measure');

And then accessing the calibration tool is pretty straightforward.

const calibrationTool = measureExtension.calibrationTool;

This tool provides valuable methods and we are going to use these three.

// Return the calibration unit
calibrationTool.getCurrentUnits();

// Return the current factor
calibrationTool.getCalibrationFactor();

// Modify the calibration factor value
calibrationTool.setCalibrationFactor([NEW SCALE FACTOR])

React to calibration event

We don’t want to get the last calibration manually, so we can subscribe to the event emitted when you finish a calibration.

     viewer.addEventListener(Autodesk.Viewing.MeasureCommon.Events.FINISHED_CALIBRATION, function(calibration){
    // Now we have the latest calibration factor so we can store it
});

Making an extension to save calibrations and set the latest

Now that we have all the functions we need, let’s create an example to demonstrate this.

To bootstrap a simple APS Viewer, I forked this repo made by Petr Broz with a NodeJS backend and vanilla JS frontend: https://github.com/petrbroz/forge-viewer-samples

I created a new html file in the public folder with the minimal code that we need to run a viewer :

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css"
        type="text/css">
    <script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js"></script>
    <style>
        body,
        html {
            margin: 0;
            padding: 0;
            height: 100vh;
        }
    </style>
    <title>Autodesk Platform Service - Calibrations Panel</title>
</head>

<body>
    <div id="forge-viewer"></div>
    <script>

        const MODEL_URN = [ADD_YOUR_MODEL_URN_HERE]
        const MODEL_3D_VIEWABLE_GUID = [ADD_YOUR_DOCUMENT_GUID_HERE]

        let viewer;

        const options = {
            getAccessToken: async function (callback) {
                const resp = await fetch('/api/auth/token');
                if (resp.ok) {
                    const token = await resp.json();
                    callback(token.access_token, token.expires_in);
                } else {
                    throw new Error(await resp.text());
                }
            }
        };

        Autodesk.Viewing.Initializer(options, async function () {
            const config = {
                extensions: []
            };

            viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('forge-viewer'), config);
            viewer.start();

            await loadModel(viewer, MODEL_URN, MODEL_3D_VIEWABLE_GUID);

        async function loadModel(viewer, urn, guid) {
            return new Promise(function (resolve, reject) {
                function onDocumentLoadSuccess(doc) {
                    resolve(viewer.loadDocumentNode(doc, doc.getRoot().findByGuid(guid)));
                }
                function onDocumentLoadFailure(code, message) {
                    console.error('Could not load document.', message);
                    reject(message);
                }
                Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure);
            });
        }

    </script>
</body>

</html>

First, we will create a custom viewer extension with a button in the toolbar. As I want to add some UI, this extension is going to open a panel to display our calibrations values and some action buttons:

class CustomCalibrationsExtension extends Autodesk.Viewing.Extension {

    constructor(viewer, options)
    {
        super(viewer, options);

        this.viewer = viewer;
        this.options = options;

    }   

    load()
    {
        this.panel = new CustomCalibrationsPanel(this.viewer, this.viewer.container, 'panelId', 'Custom calibrations panel', {});

        this.viewer.addPanel(this.panel);
        return true;
    }

    unload()
    {
        return true;
    }

    onToolbarCreated(toolbar) 
    {
        
        var panel = this.panel;

        var button = new Autodesk.Viewing.UI.Button('custom-calib-btn');
        button.onClick = function(e) {

            panel.setVisible(!panel.isVisible());
        };

        button.setIcon('adsk-icon-measure-settings');
        button.setToolTip('Custom calibrations');
            
        this.subToolbar = new Autodesk.Viewing.UI.ControlGroup('my-custom-toolbar');
        this.subToolbar.addControl(button);
      
        toolbar.addControl(this.subToolbar);

    }

}

Autodesk.Viewing.theExtensionManager.registerExtension('CustomCalibrations', CustomCalibrationsExtension);

And our custom docking panel :

class CustomCalibrationsPanel extends Autodesk.Viewing.UI.DockingPanel {
    constructor(viewer, container, id, title, options)
    {
        super(container, id, title, options);
        
        this.viewer = viewer;

        this.container.style.top = "10px";
        this.container.style.left = "10px";
        this.container.style.width = "300px";
        this.container.style.height = "350px";
        this.container.style.resize = "none";

        this.measureExt = this.viewer.getExtension('Autodesk.Measure');

        this.urn = this.viewer.model.myData.urn;
        this.guid = this.viewer.model.getDocumentNode().data.guid;

        this.calibrations = [];
        this.currentUnit = '-';
        this.calibrationFactor = 1;

        this.createPanel();


    }

    createPanel()
    {
        this.content = [
            '<div class="panel-container">',
            '<h3>Calibrations</h3>',
            '<p id="current-factor">Current scale factor: </p>',
            '<p id="current-units">Current units: </p>',
            '<table id="calibration-table">',
            '<tr>',
            '<th>#</th><th>Scale factor</th><th>Size</th><th>Unit</th><th>Set</th><th>Delete</th>',
            '</tr>',
            '</table>',
            '</div>'
        ].join('\n');

        this.scrollContainer = this.createScrollContainer();
        this.scrollContainer.style.height = 'calc(100% - 70px)';
        var childDiv = document.createElement('div');
        childDiv.innerHTML = this.content;
 
        this.scrollContainer.appendChild(childDiv);

        this.container.appendChild(this.scrollContainer);
    }

    
    onClose() {
        console.log(' Panel closed');
    }
}

Then, we load this extension after the measure extension is loaded :

           viewer.addEventListener(Autodesk.Viewing.EXTENSION_LOADED_EVENT, (event)=>{
    if(event.extensionId == 'Autodesk.Measure')
    {
        viewer.loadExtension('CustomCalibrations');
    }
})

For now, we only have a simple extension with a toolbar button that opens a UI panel. Let’s add some logic to it.

As explained before we can subscribe to the FINISHED_CALIBRATION event, to get the latest calibration and make a request to the server to store it in database. For this example, I am using a simple text file, but it can be easily improved with a database.

// CustomCalibrationsExtension.js
     this.viewer.addEventListener(Autodesk.Viewing.MeasureCommon.Events.FINISHED_CALIBRATION, async (c) => {
    await this.addNewCalibration(c); 
});


addNewCalibration = async function(calibration) {
    delete calibration.target;

    calibration.date = Date.now();
    calibration.urn = this.urn;
    calibration.guid = this.guid;

    await fetch('/api/calibrations', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
            },
        body: JSON.stringify(calibration)
    })
}


// server.js

app.post('/api/calibrations', async (req, res)=>{
    let dataFile = path.join(__dirname, 'data', 'calibrations.json');
    if(!fs.existsSync(dataFile))
    {
        let emptyJson = '[]';
        fs.writeFileSync(dataFile, emptyJson);
    }

    let newCalibration = req.body;

    if(newCalibration == null)
    {
        return res.status(400).json({err: "Payload is empty"});
    }
    newCalibration.id = crypto.randomBytes(16).toString('hex');

    let calibrations = JSON.parse(fs.readFileSync(dataFile));
    calibrations.push(newCalibration);

    fs.writeFileSync(dataFile, JSON.stringify(calibrations));

    res.status(201).json(calibrations);
})

We are able to write data to the server, so we can do the same for the reading. We use a query to filter only calibrations defined for our document.

// CustomCalibrationsExtension.js

fetchCalibrations = async function() {
    let _this = this;
    let calibrations = await (await fetch(`/api/calibrations?urn=${_this.urn}&guid=${_this.guid}`)).json();

    return calibrations;
}

// server.js
app.get('/api/calibrations', async (req, res)=>{
    let dataFile = path.join(__dirname, 'data', 'calibrations.json');
    if(!fs.existsSync(dataFile))
    {
        return res.status(404).json({err: "Not found"})
    }

    const { urn, guid }  = req.query;

    let calibrations = JSON.parse(fs.readFileSync(dataFile));

    calibrations = calibrations.filter(c => c.urn === urn && c.guid === guid)

    res.status(200).json(calibrations);
})

The different methods that I described in the first part of this article can be easily implemented to add this code functional.

getCalibrationFactor = function(_this) {
    return _this.measureExt.calibrationTool.getCalibrationFactor() || 1;
}

getCurrentUnit = function(_this) {
    return _this.measureExt.calibrationTool.getCurrentUnits();
}

setCalibration = function(_this, calibration) {
    _this.measureExt.calibrationTool.setCalibrationFactor(calibration.scaleFactor);
    _this.updateCalibrationsTable();
}

We can use the docking panel to display current values, show the different calibrations that we got from our database and implement actions in our interface.

Take a look at my demo below. I change the calibration factor from previous calibrations with a fresh new calibration and we see the measure difference. Everything done here in this 3D view will work the same in a 2D view.

The complete code of this example is available on my github.

https://github.com/AlexPiro/APS-Calibrations_Demo

© 2024 The PIROmancy blog

Theme by Anders NorenUp ↑