All about our software developer journey

Category: APS

Forma Hackathon 2024

Last week, the first Autodesk Forma hackathon was organized.

Charles and I had already met some folks from the Forma team during DevCon 2023 in Munich, and then again during AU in Las Vegas. We were encouraged to test Forma as developers and were asked to try creating extensions and provide feedback on the SDK.

Unfortunately, we were very busy with our daily work towards the end of the year and did not have time to delve into this product.

This event provided a real opportunity for us to dedicate time to Forma with the assistance of the engineering team.

The event kicked off at 9 am, and we were then all invited to pitch our ideas. Ours was quite simple – “Make Augmented Reality from Forma”.

After some discussions with familiar Autodesk faces, we began our work.

A few days before the event, I started reading the documentation and discovered from one of the examples that we were able to get triangles from the elements. That was our starting point, but after some discussions with the engineering team, they convinced us to use the Forma API – which allowed us to access the data without using an extension – and thus without opening the Forma web page.

We decided to give it a try and began making API requests to extract Forma data. Unfortunately, we quickly realized that this API was still new and needed some tips/hacks to obtain certain values.

Our biggest challenge, however, was not only competing against great teams but also against time. While we could accomplish a lot in two days, with introductions, breaks, discussions, setup, and preparation of materials for the presentation, we realistically only had about 5-10 hours of actual coding time.

Discovering the API, parsing responses, and creating recursive functions is not very complicated but can be quite time-consuming. In this case, we were not sure if we would be able to achieve our goals by the end.

Returning to our initial approach, we quickly set up a Forma extension, and with a few lines of code, we were able to retrieve the geometry of our buildings. With a basic websocket server, we could then send this data to other clients.

Next, we created a fresh Unity project – our game engine of choice for easily prototyping and developing AR apps – and added a websocket client to receive our data.

Unity is very useful for creating augmented reality apps. With the help of the ARFoundation package, we only needed to drag the necessary components into the scene to have an AR app that we could deploy on our iPad.

Now came the trickiest part: model placement. Usually, we work with markers or detected plane intersections to establish a real-world origin. However, for this example, we decided to use geolocation (the accuracy should be sufficient for this use case).

This part involved two different steps:

  1. Converting the GPS coordinates of the model to placed and the device to Cartesian coordinates.
  2. Finding the true north of the device to orient the model.

Because we were working inside the building, we did not have easy access to the outdoors, and our GPS data were not perfect. Also, due to the time limit, we streamlined this process a bit and assumed the orientation, for example, by placing the device in the right position at launch.

In any case, our presentation and work impressed the judges, and we were one of the three winners (which was amusing for us as we weren’t sure if we would even compete!).

Our source code is publicly available here: https://github.com/Piro-CIE/forma-hackathon

Our presentation slides are also available here: https://1drv.ms/p/s!Anrq2_ANoVNNjP0QKUYY9PdwVxhapQ?e=2Ijl0o

We concluded the hackathon with a drink and took the opportunity to have further discussions.

Thank you to all the Forma team for organizing this event. We were very pleased to participate in this hackathon and would like to express special thanks Daniel and HÃ¥vard.

We were also happy to see Kean, Petr and Denis. I took the opportunity to chat with them about some APS-related questions.

Our return flight was on Thursday evening, so we had some time to visit Oslo.

Olso bay view from Akershus fortress

We found a boat leaving just 10 minutes later for a 2-hour cruise, which was a great opportunity to see Oslo’s surroundings and gain a different perspective.

As avid lovers of French cuisine, we were a little disappointed to limit ourselves to Norwegian food such as pizza and sushi, so we sought out a local meal. We found a great restaurant focused on seafood.

Delicious fish and vegetables meal

It was already time to fly back to Nantes and return to our projects.

One last picture under the snow!

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 ↑