Showcase - rwth-acis/Anatomy2.0 GitHub Wiki

[TOC]

Showcase

/ https://github.com/rwth-acis/Anatomy2.0/blob/f696d/src/

  • views
    • showcase.php | showcase mainpage
    • model_viewer.php | legacy-file for avoiding dead links, redirects to showcase.php↑
    • x3d_element.php | <x3d>-tag + modelinformation overlay
    • toolbar.php | inputs like viewmode, sync on/off, annotate, …
  • js
    • showcase-base.js | loading models with ajax
    • showcase-annotations.js | everything annotation-related
    • showcase-annotations-connector.js | communication with Sevianno™ annotation-server
    • showcase-help.js | help-images with buttonclicks and keyinput
    • showcase-highlighting.js | highlighting separate parts of the model
    • showcase-specials.js | effects on a per-model basis, e.g. fading skin of a head
    • showcase-sync.js | yjs-implementation for syncing a role-space
    • showcase-toolbar.js | toolbar and MVVMs
    • x3d-extensions.js | extended x3dom™ interaction
  • css
    • showcase.css
  • widgets
    • showcase.xml | role-wrapper for showcase.php↑
    • showcase_placeholder.xml | template for showcase.xml↑

Annotations

Persistent storage

Sevianno™ web-service is used to store annotations. It is hosted in i5, originally it is used to create video annotations. The create, read, update and delete calls are implemented in howcase-annotations-connector.js↑.

The service does not provide predefined fields for all needed values, e.g. there is no predefined pos field in Sevianno. As the service uses ArangoDB for storage custom fields can be added your annotation. Just create a custom attribute in the (JSON encoded) object to send to Sevianno. When receiving an answer from Sevianno Service, you will find your custom attribute in the annotationData attribute of the received JSON object.

Sevianno ID: Sevianno creates IDs for all objects it stores. Therefore, each annotation has an ID. In addition, annotations are attached to objects (in our case: models), which also have an ID. Sevianno object IDs for models are stored in Anatomy2.0 database. After initially reading the model ID from our database, showcase-annotations.js↑ will keep track of all Sevianno IDs it receives.

Local ID: When creating a new annotation in the model viewer, it takes some time until Sevianno stored the new annotation and returns a Sevianno ID. Of course, we have to show the annotation in the frontend before storing the annotation with Sevianno is finished. Now, the user might add multiple annotaions during this timespan. When receiving a response from Sevianno with a Sevianno ID, we now need to be able to map the response to one of the created annotations. For this reason, showcase-annotations.js↑ creates local IDs (which are stored as part of the annotation on Sevianno). When receiving a response (which will contain a local ID), model viewer is able to match the response to one of the new annotations.

What is stored with Sevianno? For each annotation, its position, title, content, username and localId is stored with Sevianno Service. The position of an annotation is stored with two values. First, the 3D coordinates of the position pos of the cone (more precisely the position of the apex of the cone) is stored. Secondly, the 3D vector of the direction norm of the cone is stored. The last editor is stored as the user name (givenName + ' ' + lastName). Model viewer has a helper function personality.getCurrentUsername to receive the name of the currently logged in user.

Displaying annotations

x3dom Scene Graph and "How to access annotation markers?" In the x3dom scene graph an annotation marker will be represented by 4 elements. There is a <transform> node which positions the annotation marker in 3D space. A <shape> node is attached to the <transform>, which represents the 3D geometry of the annotation marker. The <shape> node has a <cone> child and an <appearance> child node which contains a <material> node. Adam found out, that the <material> node is required for lighting calculations to take place. The <material> node defines all values for lighting calculation (e.g. diffuse color and transparency).

The annotationMarkers.elements object stores a reference to all annotation markers by annotation id. Local ids are used if there is no Sevianno ID yet. annotationMarkers.elements will store the <transform> node of each annotation marker. You have to navigate via the children[i] attribute to its descendant nodes.

Annotation marker appearance: Anatomy 2.0 provides the feature, that annotations are color coded to show their editor. In other words, the markers of two annotations will have diffent colors if they have been last edited by two different users. Of course, annotations created by the same user will have the same color. This way, it is easy to see which annotations have been created by the same user.

The color of each annotation marker can be set using its <material> node. The model viewer saves a material node for each user who has at least one annotation on the currently viewed model. The materials are stored in the annotationMarkers.materials object by username. When showing an annotation marker, the function annotationMarkers.getMaterial() is used to determine the annotations look. If there is no material for the user yet, a new one is created.

Note: It is not possible to assign the same material to multiple annotation markers, because x3dom will remove the material node from the shape it has been previously attached to. Therefore, <material> nodes are always copied before assigning them to a node. This is already handled by annotationMarkers.getMaterial() function.

Content box: There needs to be some place to display detailed information about an annotation. Anatomy2.0 has a content box to display detail information for annontations. There is one content box statically defined in HTML code called annotation-content. One set of HTML elements is sufficient, because there is always just one content box visible at a time.

The content box has three different modes to be able to react to user interaction. The modes are loading, read and edit. The content box is in read mode by default. loading is shown if and only if the user tries to open a content box, but the annotation is not yet in local storage (it is still being created and / or read from Sevianno Service). Switching modes is done with the annotationContentBox.switchMode() which accepts a String as argument. The argument has to be one of the three mode names.

When a user clicks an annotation marker, the annotation content box has to be positioned somewhere near the marker. But it should not overlap with the marker or the model part which is annotated. Anatomy2.0 has a simple algorithm which tries to approximately achieve this behavior. The annotation content box is initially placed at the position where the user clicked the annotation marker and from there it is shifted to the border of the screen. The positioning is handled by annotationContentBox.calcPosition(). Note, that the content box is not repositioned when the user rotates or moves the model on the screen.

x3dom™ integration

The framework powering 3d-rendering and interaction is x3dom™. A basic example to integrate in an html-page is

<x3d width='500px' height='400px'>
    <scene>
        <shape>
            <appearance>
                <material diffuseColor='1 0 0'></material>
            </appearance>
            <box></box>
        </shape>
    </scene>
</x3d>

An x3d-file is ajax-imported with

<x3d>
  <scene>
    <inline nameSpaceName='inlinespace' id="x3dInline" url="../../my3dObject.x3d" onload=showcase.onModelLoaded> </inline>
  </scene>
</x3d>

The textures for that model are defined in the x3d-file and autoloaded by x3dom™ ajax-requests.

The onload-function is overwrtten directly in the <inline>-tag because it seems not to support addEventListener. The function showcaseo.onModelLoaded notifies all observers previously added with showcase.addEventListener('load', someCallbackFunction).

The <inline>-tag's onclick-functionality is not supported by jQuery, so that its addEventListener must be used.

Implementation of some functionality for changing the 3d-camera-position (viewport) is in x3d-extensions.js↑. Implementation of observing changes is the viewport are in showcase-sync.js↑. Implementation of fading an object via transparency and showing an untextured object is in showcase-specials.js↑.

Synchronisation

Synchronisation is powered by Y-js. It is implemented in showcase-sync.js↑.

Initialisation

Y({
  db: {
    name: 'memory'
  },
  connector: {
    name: 'websockets-client',
    room: 'Anatomy2.0-role-v1.1.0-'+URI().query(true).rolespace,
  },
 sourceDir: location.pathname + '/../../external',
 share: {
     showcase: 'Map'
 }
}).then(function (y) {
  // code that runs after synchronisation is established
}

The expression URI().query(true).rolespace extracts the queryparam ...views/showcase.php?rolespace=zahnkurs. URI™ is an external bower-dependency. So every course has its own synchronisation context.

Yjs™ stores its content on the XMPP-server used to connect the clients when they load the page. On changing implementation, v1.1.0 can be changed to v1.1.1 so that each context is emptied.

To interact with the y-object:

// one direction
someUserInteractionCallback = function (evt) {
    if ( synchronisationNotWishedForWhateverReason ) { return }

    y.share.showcase.set('some_key', someValue)
}

// the other direction:
y.share.showcase.observePath(['lecturer_mode'], someYEventCallback)

Synchronisation-initialisation is divided in three asynchronous timeperiods:

var onDocLoad = []
var onYLoad = []
var onModelLoad = []

onDocLoad.push( someInitFunction )
onDocLoad.push( someInitFunction )
onDocLoad.push( someInitFunction )

onYLoad.push( someInitFunction )
onYLoad.push( someInitFunction )


// document-loaded
$(document).ready( function () {
    showcase.sync.onDocLoad.forEach(function(callback){callback()})
})

// Y loaded
Y( {} ).then(function (y) {
    

    // catching errors because else Y-js would eat them
    try { showcase.sync.onYLoad.forEach(function(callback){callback()}) }
    catch(err) { console.log('Error in Y-load callbacks: ', err)}
})

// model loaded
showcase.addEventListener('load', function () {
    

    showcase.sync.onModelLoad.forEach(function(callback){callback()})
})

This approach rather than

$(document).ready( function () {
  // one big init
})

Y( {} ).then( function() {
  // another big init
})

showcase.addEventListener('load', function () {
  // the last big init
})

was chosen to group logical chunks inside the code, e.g. so that

// viewport: remote → local
onYLoad.push(function () {
     // setup some reaction to remote viewport change
})

// viewport: local → remote
onModelLoad.push(function () {
     // setup some reaction to local viewport change
})

can be written one after the other in the sourcecode.

MVVM

If e.g. someone selects a new object to be shown on everyone's device it must be consistent in

  • Y-Object
  • toolbar (ViewModel + View)
  • program state (Model).
// showcase-toolbar.js, place of the MVVMs
var modelId = ko.observable( someModelId )

modelId.subscribe( function(newModelId) {
     // show the new model
})

// overview-widget.js

$('.img-list').find('a').on('click', function () {
            modelId( $(this).prop('modelId') )
})

// showcase-sync.js

// ViewModel → Y-object propagation
modelId.subscribe( function (newModelId) {
    y.share.showcase.set('selected_model', newValue)
})

// Y-object → ViewModel propagation
y.share.showcase.observePath(['selected_model'], function () {
  modelId( y.share.showcase.get('selected_model') )
})

For MVVM, Knockout™(ko) is used with its API:

var viewModel = ko.observable( someModelId )

viewModel.subscribe( function(newValue) {
    // update view
})

// e.g. on a click event:
viewModel( someValue )

The properties

  • isSynchronized (default: false)
  • lecturerModeViewModel.modeEnabled (default: false),
  • modelId (default: as defined in url)

are synchronized with the y-object.

⚠️ **GitHub.com Fallback** ⚠️