Clirio Scan Share
Scan Share is a feature of the Clirio product suite, for sharing photogrammetry scans quickly, and displaying them in a webviewer.
Overview
The Clirio Scan Share feature lets users quickly share a photogrammetry scan captured with the Clirio Scan app. The feature is comprised of share options in the Clirio View app developed in Unity and a webviewer developed in Blazor and Three.js, through which shared scans can be viewed. Pressing the share button on any of the Clirio View apps will open a pop-up to configure the share. The user has the option to set expiry, password protection, and to require that the viewer has been invited to the workspace, in which case the recipient will need to log in with their Clirio account to view the scan.
Once the user is satisfied with their selected options, they can generate the link, and either copy it to the clipboard or use the share function to trigger the share options native to each platform. For instance, on iOS this will open the share dialogue with compatible apps such as messengers, while on Windows the mail app will open. There is also an option for users to configure and generate a formatted embed iframe element with the share.
The webviewer through which recipients can view the scan is intended to be as lightweight and accessible as possible. The scan can be viewed from different angles, some metadata and a scale legend is displayed, and there are some buttons to hide or show elements.
Development
There were two stories for the development of this feature: the clientside app share UI and logic, and the webviewer.
Share UI and Logic
Developed with C# in Unity, as a pop-up accessible through the observation details view of any scan in the Clirio View app. UI was designed in Figma in collaboration with Jordan Wischmann.
Webviewer
Developed with Blazor and Three.js. Backend token sharing was implemented by Timothy Thibault.
Share UI and Logic
The share UI has gone through multiple iterations. Initially developed for MRTK2 with few options, the feature was later expanded to include expiry, password protection, and embed functionalities. Finally, in 2023, the UI was refreshed in collaboration with Jordan Wischmann, who designed mockups in Figma, as the Clirio View app was migrated to MRTK3.
Timothy Thibault. The returned result includes a URL for the client to display to the user, and to include in native share options for each platform.
The biggest challenge for me was designing the UX, as there were a lot of options for the user. This included the option to make shares private so that users could restrict sharing internally to teams in workspaces, which required an additional authentication flow in the webviewer as well. The range of options also made it more difficult to design a compact UI. I utilized dynamic design and layouts so that hidden options such as expiry renewal or password change could be elegantly hidden or shown. Developing the logic was a fairly easy process, thinking about how the feature should work in the first place was the bigger challenge. After every user interaction, the UI refreshes to match the share configuration and all other states. If there is a share matching the configuration, that existing link is shown, otherwise the generate share button is shown.
An interesting change to my approach to writing controllers was first tested in this feature, after a discussion I had with my teammate Tonio Stillman. OnClick() event. Here, I instead set all the event listeners in the controller itself, kept all the corresponding listener functions private, and simplified the inspector setup so that each button only gets assigned once to the controller, and no other reference is needed. This makes setup in the editor less error-prone and maintainable, as it requires less understanding of the code.
Webviewer
Implementing the webviewer was the more complex story, as it was standalone and most functionality needed to be created from scratch. The viewer was written in C# with Blazor, JavaScript with Three.js, and uses Tailwind CSS for styling. I had worked with Three.js in the past and enjoyed using it again, and a lot of my work revolved around setting up the viewer correctly.
Challenges with the setup of Three.js included adding support for loading the texture files defined in the MTL file with the correct SAS tokens, ensuring different MTL file formats and material properties are supported and displayed as desired, and making the viewer dynamic so it supports a wide range of model sizes and shapes, on a wide range of devices.
Code Sample
// CODE SNIPPET, NOT THE FULL FILE ////______________________________________________________________________ Loadfunction loadModel(OBJFile, MTLFile, SasUrls) { //Load with MTL + Textures //Note: The MTLLoader has been customized so that it can have a SAS token specified, // and that it converts the filenames in the MTL file to lowercase(otherwise Azure Storage URL will be incorrect) var mtlLoader = new MTLLoader(); mtlLoader.setSasUrls(SasUrls); mtlLoader.load(MTLFile, function (materials) { materials.preload(); new OBJLoader() .setMaterials(materials) .load(OBJFile, function (object) { object.traverse(function (child) { // aka setTexture if (child instanceof THREE.Mesh) { child.material.side = THREE.DoubleSide; //The default color space is sRGB, we need Linear (will be too dark otherwise) //see https://threejs.org/docs/#manual/en/introduction/Color-management if (child.material.map) child.material.map.encoding = THREE.LinearEncoding; } }); object.rotation.y = Math.PI; //rotate 180 degrees to match orientation in Clirio View //Calculate y center pos, raise model and recenter camera on model var boundingBox = new THREE.Box3().setFromObject(object); size = new THREE.Vector3(); boundingBox.getSize(size); var center = new THREE.Vector3(); boundingBox.getCenter(center); var centerPos = object.localToWorld(center); objYCenterPos = size.y / 2 - centerPos.y + gapAboveGrid; longestSide = Math.max(size.x, size.y, size.z); object.position.set(centerPos.x, objYCenterPos, centerPos.z); //for large objects, update view + grid configureGridBasedOnModelSize(); resetView(); //add to scene, add scale legend, remove spinner scene.add(object); addScaleLegend(); scanSpinner.classList.add('hidden'); }, function (item) { //console.log("Load Progress: " + item.loaded + "/" + item.total); }, function (error) { console.error(error); }); }); animate();}