Infinite API v4.1.24

Infinite API

A TypeScript/JavaScript API to use the 3dJuump Infinite technology.

Table of content

Introduction

This documentation describes the use of the TypeScript/JavaScript 3dJuump Infinite API.

Getting started

In order to use the 3dJuump Infinite API, you just need a valid 3djuump Infinite architecture. We assume the 3djuump Infinite directory is requested from https://my_directory. You need to include a script markup that will include the Infinite api from https://my_directory/directory/api/getwebapi?version=4.1 and begin the coding of your application.
If you upgrade the 3djuump Infinite server to a new version, you do not need to change anything to your code, the implementation of the 4.1 api version on the new 3djuump Infinite architecture will be available. But if the change concerns a minor version increment then keep in mind that you will not be able to take advantage of the new functionalities (see Migrating from 4.0 version).

You may want to use TypeScript to code your application, then create a .npmrc file in your base folder and set :

 @3djuump.com:registry = https://npm.3djuump.com

The required package is @3djuump.com/infiniteapi@4.1. It is strongly advised to install the @3djuump.com/infiniteapi version of your Infinite development server (including patch version), as new functionalities may be added during the development phase. Assuming your development server is of version 4.1.0, then make :

npm install "@3djuump.com/infiniteapi@4.1.0"

If you are not sure which version your server is using, browse to https://my_directory/directory/api/getversion :

{
"comment":"directory http api",
"type":"directoryapi",
"version":
{
"backend":"4.1.0.2055 #cc2f93d8",
"clientfeature":"7.0",
"number":"4.1"
}
}

The version/backend field tells to use 4.1.0, then install the 4.1.0 version. If your server is updated to a new patch version, then install the new patch version, as new functionalities may be available (see the version log for more information).

If you do not comply with this rule and install the 4.1 version, you will get the last api definition, but your server may be in a lower revision, meaning that you may use functions not already available in you current server, which may resulting in using undefined functions.
(you may also get the api version by browsing the api documentation available in https://my_directory/directory/webapidoc, but keep in mind that your administrator may have removed the online documentation).

Begin importing with :

import { Vector3 } from '@3djuump.com/infiniteapi';

If you are using a javascript bundle tool such as webpack or vite, do not forget to add @3djuump.com/infiniteapi as external, that will be used with the infiniteapi global namespace.

Webpack:

externals: {
"@3djuump.com/infiniteapi": "infiniteapi",
},

Vite with vite external plugin:

  plugins: [
viteExternalsPlugin({'@3djuump.com/infiniteapi': 'infiniteapi'}),
...
]

The same holds if you are using a js loader such as require, etc ...

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">

<head>
<title>My Application</title>
</head>

<body>
<div id="mainContainer">
...
</div>
<script type="text/javascript" src="https://my_directory/directory/api/getwebapi?version=4.1"></script>
<script type="text/javascript" src="myscript.js"></script>
</body>
</html>

The 3dJuump Infinite API includes opentype.js, , luxon, bowser and xmldom (included in the js file and web-workers). The 3dJuump Infinite API only uses vanilla JavaScript, and does not redefine any public API. Thus, you may include any framework of your liking without fear of incompatibilities with the API.

The api documentation may be available through https://my_directory/directory/webapidoc.

The 3dJuump Infinite API features the main following modules :

  • Main : this modules defines common interfaces used by all other modules.
  • Events : the 3dJuump API makes an intensive use of the Observer pattern. Many objects send signals when changes occur, the different events and their content is explained in this module.
  • Sessions : this module handles the authentication to the 3dJuump Infinite backend.
  • Factories: this module allows to create the main objects that are used to communicate with the 3dJuump Infinite architecture.
  • Maths : this module includes multiple classes that manipulate multi-dimensional vectors and matrices. This module contains the necessary classes to manipulate 3D data.
  • 3D Rendering : this module allows to display the 3D data of the DMU (geometries, cameras etc ...).
  • 3D Primitives : this module allows to display/add simple 3D primitives (boxes, lines, points) in the DMU.
  • Metadata : this module features the main objects to filter/gather a set of geometries.
  • Metadata/Filters : this sub-module of the Metadata module includes all the different types of filter that may be used to create sets of part instances.
  • Data Retrievers : this module includes multiple objects that may retrieve information about part instances.
  • Converters : this module allows converting different types of ID. The ID system is explained below.
  • AsyncJobs : this module contains heavy computations that are performed by the 3dJuump Infinite backend such as the 2D/3D export procedures.
  • Features: this module is used to display specific detected features of 3d geometries (circle / lines) and performs 3D measurements.

A sample explaining the basic concepts is shipped with the given documentation. Do not hesitate to refer to the file sample.ts (TypeScript version) and sample_js.js (vanilla JavaScript version).

Requirements

You will need the following :

  • a working 3dJuump infrastructure (a 3dJuump Infinite directory and a 3dJuump Infinite proxy, and one working 3dJuump Infinite build).
  • The 3dJuump Infinite directory contact address will be identified as https://my_directory:443/directory. The api documentation may be available through https://my_directory:443/directory/webapidoc.
  • your clients (and the developers) must be valid users in the 3dJuump Infinite directory.
  • the authentication URL of the application you will develop must be registered onto the 3dJuump Infinite Directory (https://my_directory/directory/#/applications, and one of your registered application must have your URL registered : your application will be available on the your_server server and in the your_application folder. If you plan to use popup based authentication, you may need to create a page authenticate.html and add https://your_server/your_application/authenticate.html to the list of allowed authentication URLs. If you plan to use a same page authentication, you will need to add your main page (without url parameters) to the list of allowed authentication URLs.
  • Popup based authentication : you may need to write 2 scripts and 2 html files : one script and one html to handle your application logic, and one script and one html to handle authentication (in our case authenticate.html).
  • Same page based authentication : you will need to write 1 script and 1 html file : one script and one html to handle your application logic.

Registering / Enabling your application

As with the configuration of the sample, your application url (redirect url) must be trusted from the directory in order to allow the authentication mechanism. Lets say your application is available though https://my_server/myapplication/index.html with a same page authentication mechanism, your 3djuump Infinite administrator must create a new application onto the directory front-end (https://<directory>/directory/frontend), and register your url https://my_server/myapplication/index.html in the trusted list.

registering application
  • If your redirect url, if called without a hash, lead to your main application page
  • and if your favicon is available through :
    • your redirect url is a folder (e.g. https://my_server/myapplication) => https://my_server/myapplication/favicon.ico.
    • your redirect url is a file (e.g. https://my_server/myapplication/index.html) => https://my_server/myapplication/favicon.ico.

Then your application shortcut (along with its icon) will be available from https://<directory>/directory or https://<directory>/directory/home.

applications list

Testing / Installing / Configuring the sample

The 3dJuump JavaScript web API is shipped with a code example.

Installation of the sample

Unzip the samples folder of the infinite_api_<version>.zip to a folder of your liking. The sample is now shipped with a package.json. Make sure node.js is available in the path. The current nodejs version is now 20.18.0. The sample is using webpack with a webpack dev server. Just run npm i. Find and replace in all the included files all occurrences of ${directoryUrl} and replace them by the base URL of your directory (without the leading /directory). Run npm run dev. You have now a dev server running on http://127.0.0.1:8090. Make sure http://127.0.0.1:8090/index.html (same page authentication) and http://127.0.0.1:8090/authentication.html (popup based authentication) are registered as valid redirection urls in the Applications settings of your directory. Allow popups from http://127.0.0.1:8090/index.html (popup based authentication).

Test/browse to http://127.0.0.1:8090/index.html, you should see the sample application running.

Security considerations

The default webpack server is setting Content Security Policy headers to ensure a minimum of security. Please see the webpack.config.js file to view the used CSP.

Configuration of the sample

Edit the file samples/current/data.js :

// Custom Configuration Part : do as you wish
// should we test the typescript sample (require) or vanilla javascript ?
window['use_typescript'] = true;
// which authentication system to use ? popup or same page ?
window['authentication'] = 'popup';
// window['authentication'] = 'samepage';

// The picking test :
// 'normal' => geometry are selected
// 'box' => needs two picks : one for box center, the other for box extent and box creation
// 'line'=> needs two picks : one for point 1, one for point 2 and line creation
// 'point' => a point is created at the 3d position
window['pickingTest'] = 'normal';

// some test values
window['test'] = {
// the sample will search on 'roue', sorry we are with a french DMU :)
'searchTerm': 'roue',
// the filterAttribute will try to find parts with attribute name 'Name' that contains the values
// 'elec' or 'Train AV Link&Go' (it is actually inverted)
'attributeFilterContains': {
'attributeName': 'Name',
'values': ['elec', 'Train AV Link&Go']
},
// the filterAttribute will try to find parts with attribute name 'system' that have the exact value
// '4000-Inter'
'attributeFilterExact': {
'attributeName': 'system',
'values': ['4000-Inter']
},
// the boxFilter will select parts that are in the box centered on 'center'
// and half extent 'half_extent'
'boxFilter': {
'center': [1234, 0, 734],
'half_extent': [1500, 1500, 1500],
},
// partInstanceList filter will select parts that have the value 'value'
'partInstanceListFilter': { 'value': 786 }
};

The data shown here is only the relevant section of the configuration of the sample :

  • use_typescript tells if you want to test the sample.ts file (TypeScript) or sample_js.js. Depending of your web language, i.e. vanilla JavaScript or TypeScript, you may want to use one or the other.
  • authentication tells the way authentication should be handled : popup based or same page.
  • pickingTest tells if the picking should be used to select geometries ("normal"), create 3D points ("point"), create 3D lines ("line") or create 3D boxes ("box")
  • the test property may be edited depending on the available DMU on your 3dJuump Infinite server. The sample tests 2 string metadata filters, a search request roue, a box request (you may change the values depending on your DMU) and a specific part instance (here part instance id : 786).

Testing of the sample

Choose your DMU and you can see the content of your DMU.
Keyboard shortcuts :

  • space : re-centers the DMU and clears ghost and selection.
  • C : cycles through the available configurations of the DMU.
  • F : cycles through metadata filters test : string metadata, box, part instance ids list
  • S : triggers a search.
  • K : replaces the material of the last picked element by a red material, or restores the original material of the last changed element.

A note on web-workers and security

The infinite api is now using web-workers. Due to security considerations, it is not possible to load a web-worker directly from a site that does not have the same origin as the main page (CORS). For this reason, the infinite api is now shipped with a very simple web-worker loader (infiniteapi-webworker-loader.js) which role is to load the correct web-worker of the infinite api. In order to use web-workers, the infiniteapi-webworker-loader.js file should be available from the same origin as the application, and the api tries to load window.origin/lib/infiniteapi-webworker-loader.js by default. You may override the default web-worker loader location by using the InfiniteApiControllerInterface.setWebWorkerPath function. The loader file is available through /directory/webapi/infiniteapi-webworker-loader.js, the infinite_api_<version>.zip file, or the npm package of the infinite api. You may safely include it in the application code base, this will load the correct web-worker script. Using this script ensures that the api loaded from https://my_directory/directory/api/getwebapi?version=4.1 is compatible with the web-worker script. Using this scheme allows your application to be always ready to accept regular updates without changing anything in the code base. You may set CSP headers, namely script-src-elem to your application and the directory urls to make sure only trusted scripts are loaded.

Authentication Samples

In order to get started, the following codes explains the basics of connecting to a 3dJuump Infinite build.

Tip : How to choose between same page authentication and popup based authentication ? The popup based authentication do the authentication process in another window/tab, therefore another window will be spawned. This authentication type preserves the application context from being erased during authentication process and rely on the LocalStorage API to transfer data session from the spawned window once authentication is finished to the application window. Warning, popups may be unauthorized on your client browser.
For the same page authentication, since the authentication process takes place inside the same page through redirects, it requires that the developer keeps track of a previous application context if necessary.

Same page authentication

Here the sample uses the sessionStorage of the browser to handle the final redirection. Indeed, your application may features multiple URLs, but you will not register every single URL of your application onto the InfiniteDirectory. Instead, you will register your main URL onto the InfiniteDirectory, and store the final URL in the sessionStorage of the browser, in order to redirect to the correct URL. You may also use the DirectoryAuthenticationOption.applicationData with DirectorySessionInterface.getSamePageAuthenticationUrl.

/** 
* Sample to illustrate the same page authentication mechanism.
*/
import {
InfiniteFactory, InfiniteCacheFactory, DirectorySessionFactory,
InfiniteEngineInterface, InfiniteCacheInterface, DirectorySessionInterface,
InfiniteEvent, ConnectionData,
DataSessionInterfaceSignal, DirectorySessionInterfaceSignal,
} from 'generated_files/documentation/appinfiniteapi';

// the current page (without url parameters) should be registered as authentication url in the directory
const sDirectoryUrl : string = 'https://my_directory:443/directory';

let lInfiniteEngine: InfiniteEngineInterface | undefined = undefined;
let lCache: InfiniteCacheInterface | undefined = undefined;
let lDirectorySession: DirectorySessionInterface | undefined = undefined;

// what to do to authenticate (authenticate function may be bound to a div `click` or called straight away)
let authenticate : () => void = undefined;

// Checks if the url has an hash in order to authenticate
let checkHash : () => void = undefined;

// Success callback on login
let onLoginSuccess : (pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void;

// Success callback when DataSession is ready
let onDMULoaded : (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void;

// *****************************************************************************
// ******************************* Authentication ******************************
// *****************************************************************************
// what to do to authenticate (authenticate function may be bound to a div `click` or called straight away)
authenticate = () : void =>
{
// if we have a hash, do not request a new authentication
if (window.location.hash.length > 0)
{
// perhaps some GUI code there
return;
}
// connect to directory with address sDirectoryAddress, in the folder "/directory" through https. The default port is unchanged (443)
lDirectorySession = DirectorySessionFactory.CreateDirectorySession(sDirectoryUrl);

// redirect url is the current url without query parameters
// the directory needs a full url (with query parameters if needed), so we recommend to
// register the url without parameters, use this url as authentication, store the url parameters if present
// and then redirect to the "final url" (see checkHash)
const sRedirectUrl : string = window.location.origin + window.location.pathname;
if (window.location.href.includes('?'))
{
// store the url with query parameters in session storage
// we will finally redirect to this url
window.sessionStorage.setItem('redirecturl', window.location.href.split('#')[0]);
}
const sURL = lDirectorySession.getSamePageAuthenticationUrl(sRedirectUrl);
if (typeof sURL === 'string')
{
// navigate to the given url for authentication
window.location.href = sURL;
}
else
{
console.log('Error not expected !!! ' + sURL);
window.sessionStorage.removeItem('redirecturl');
}
};

// Checks if the url has an hash in order to authenticate
checkHash = () : void =>
{
const lHash : string = window.location.hash;
if (lHash.length === 0)
{
// do not bother if no hash
return;
}
// we may here put some GUI code

// here we can check if the url has query parameters
const lSessionItem : string | null = window.sessionStorage.getItem('redirecturl');
if (lSessionItem)
{
// we have a redirect url
// remove it from storage in order to avoid infinite redirects
window.sessionStorage.removeItem('redirecturl');
// navigate to this url with hash
window.location.replace(lSessionItem + lHash);
}
else
{
// remove hash with some fancy methods
if (window.history.replaceState)
{
const uri : string = window.location.toString();
const cleanUri : string = uri.substring(0, uri.indexOf('#'));
window.history.replaceState({}, document.title, cleanUri);
}
else
{
window.location.hash = '';
}

lDirectorySession = DirectorySessionFactory.CreateDirectorySession(sDirectoryUrl);
// do something when we are connected !!!! => onLoginSuccess
lDirectorySession.addEventListener(DirectorySessionInterfaceSignal.LoginSuccess, onLoginSuccess);
// and end authentication
lDirectorySession.setTokenFromHash(lHash);
}
};

// Success callback on login
onLoginSuccess = (pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// Create a 3D engine to handle the rendering of the DMU. The engine is optional if you
// just want to search data in the DMU, but required to display 3D data.
// you need to provide a div inside your html file, or create one div programmatically
lInfiniteEngine = InfiniteFactory.CreateInfiniteEngine();
// bind the engine to the given div that will be used for rendering
lInfiniteEngine.setView(document.getElementById('rendering'));
// Create a cache to avoid requesting heavy data from the server if data has been retrieved already
lCache = InfiniteCacheFactory.CreateInfiniteCache();
// Get the Connection Data to get the build lists
const lConnectionData: ConnectionData = <ConnectionData>pEvent.attachments;

// Get the list of projects => do something as you like
const lProjectList = lConnectionData.projects;
for (const lProjectId in lProjectList)
{
// iterate but get the first project
const lBuildList = lProjectList[lProjectId].builds;
for (const lBuild in lBuildList)
{
// iterate but get the first build
// create a data session that uses the cache with the first item
const lDataSession = (<DirectorySessionInterface>(pEvent.emitter)).createDataSession(lBuild, lCache);
if (lDataSession)
{
lDataSession.bindInfiniteEngine(lInfiniteEngine);
// be ready to do something when build is ready (i.e. all init data has been parsed and server is ready)
lDataSession.addEventListener(DataSessionInterfaceSignal.DMULoadingSuccess, onDMULoaded);
// and go !!! => open the data session
lDataSession.openDataSession();
}
return;
}
}
};

// Success callback when DataSession is ready
onDMULoaded = (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// do something !!!
// create filters
// change visibility
// etc ....
};

// we may launch an authentication procedure (or from a button)
authenticate();

// check hash if we should validate the authentication
checkHash();

Popup based authentication

/** 
* Sample to illustrate the popup based authentication mechanism.
*/
import {
InfiniteFactory, InfiniteCacheFactory, DirectorySessionFactory,
InfiniteEngineInterface, InfiniteCacheInterface, DirectorySessionInterface,
InfiniteEvent, ConnectionData,
DataSessionInterfaceSignal, DirectorySessionInterfaceSignal,
} from 'generated_files/documentation/appinfiniteapi';

// authentication url as defined in the directory
// authenticate.html must include your authentication script
const sAuthenticationUrl : string = 'https://your_server/your_application/authenticate.html';
const sDirectoryUrl : string = 'https://my_directory:443/directory';

// Create a 3D engine to handle the rendering of the DMU. The engine is optional if you
// just want to search data in the DMU, but required to display 3D data.
// you need to provide a div inside your html file, or create one div programmatically
const lInfiniteEngine: InfiniteEngineInterface = InfiniteFactory.CreateInfiniteEngine();
// bind the engine to the given div that will be used for rendering
lInfiniteEngine.setView(document.getElementById('rendering'));
// Create a cache to avoid requesting heavy data from the server if data has been retrieved already
const lCache: InfiniteCacheInterface = InfiniteCacheFactory.CreateInfiniteCache();

// what to do to authenticate (authenticate function may be bound to a div `click` or called straight away)
let authenticate : () => void = undefined;

// Success callback on login
let onLoginSuccess : (pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void = undefined;

// Success callback when DataSession is ready
let onDMULoaded : (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void = undefined;

// *****************************************************************************
// ******************************* Authentication ******************************
// *****************************************************************************
// what to do to authenticate (authenticate function may be bound to a div `click` or called straight away)
authenticate = () : void =>
{
// connect to directory with address sDirectoryAddress, in the folder "/directory" through https. The default port is unchanged (443)
const lDirectorySession: DirectorySessionInterface = DirectorySessionFactory.CreateDirectorySession(sDirectoryUrl);

// do something when we are connected !!!! => onLoginSuccess
lDirectorySession.addEventListener(DirectorySessionInterfaceSignal.LoginSuccess, onLoginSuccess);

// retrieve the url to contact to begin authentication with popup
// that also starts the authentication procedure
const sURL = lDirectorySession.getPopupBasedAuthenticationUrl(sAuthenticationUrl);
if (typeof sURL === 'string') {
if (sURL !== '' && sURL.includes('authenticate'))
{
// popup based authentication
window.open(sURL);
}
}
};

// Success callback on login
onLoginSuccess = (pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// Get the Connection Data to get the build lists
const lConnectionData: ConnectionData = <ConnectionData>pEvent.attachments;

// Get the list of projects => do something as you like
const lProjectList = lConnectionData.projects;
for (const lProjectId in lProjectList)
{
// iterate but get the first project
const lBuildList = lProjectList[lProjectId].builds;
for (const lBuild in lBuildList)
{
// iterate but get the first build
// create a data session that uses the cache with the first item
const lDataSession = (<DirectorySessionInterface>(pEvent.emitter)).createDataSession(lBuild, lCache);
if (lDataSession)
{
// bind the given data session to the Infinite engine
lDataSession.bindInfiniteEngine(lInfiniteEngine);
// be ready to do something when build is ready (i.e. all init data has been parsed and server is ready)
lDataSession.addEventListener(DataSessionInterfaceSignal.DMULoadingSuccess, onDMULoaded);
// and go !!! => open the data session
lDataSession.openDataSession();
}
return;
}
}
};

// Success callback when DataSession is ready
onDMULoaded = (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// do something !!!
// create filters
// change visibility
// etc ....
};

authenticate();

As for authentication, the following code handles the authentication :

/** 
* Sample to illustrate the use of the authentication in the popup based authentication.
*/
import { DirectorySessionFactory } from 'generated_files/documentation/appinfiniteapi';

if (window.location.hash)
{
// try to decode the authentication hash.
if (DirectorySessionFactory.DecodeAuthenticationHash(window.location.hash))
{
// everything is fine, the other tab is notified
// just need to close the tab
window.close();
}
else {
// output some debug error message.
console.log('Error: unable to decode hash content of authentication');
// display error
const lError: HTMLElement = document.getElementById('error');
lError.textContent = 'Unable to decode hash content of authentication';
}
}
else
{
// we have no hash => go back to main page
// get the url content
const lPath = window.location.pathname;
const lOffset : number = lPath.lastIndexOf('/');
// replace 'authentication.html' by main page 'index.html'
if (lOffset >= 0)
{
// create new location
const sLocation = window.location.origin + lPath.substring(0, lOffset) + '/index.html';
// ang go
window.location.href = sLocation;
}
}

Main concepts

Observer pattern

Many objects in the API trigger events, the developer just needs to connect to these events in order to be notified of key events and react accordingly. The Observer pattern is provided by the EventDispatcherInterface. The 3djuump Infinite API uses a system very similar to DOM events with the EventDispatcherInterface.addEventListener and EventDispatcherInterface.removeEventListener functions.

/** 
* Sample to illustrate some CameraManagerInterface signal system.
*/
import { InfiniteEngineInterface, CameraManagerInterfaceSignal, tListenerCallback } from 'generated_files/documentation/appinfiniteapi';

// engine has been created previously
let lEngine : InfiniteEngineInterface;
// callback called when the camera location is changed
let myCallback : tListenerCallback;

lEngine.getCameraManager().addEventListener(CameraManagerInterfaceSignal.CameraLocationChanged, myCallback);

Infinite Objects

Many objects in the api are derived from the InfiniteObjectInterface, this allowing to know the type of the object and a way to clear the associated resources with InfiniteObjectInterface.dispose. Once disposed, any subsequent call to the given object will be discarded with a message appended to the console. The main Infinite Object is the InfiniteApiControllerInterface singleton, that provides access to the font loader and logging facilities.

Documents

The 3dJuump Infinite database is composed of numerous documents. There are several types of document that are contained in a DMU. The DMU is basically a collection of parts that reference themselves in a child/parent relationship. A part may have a geometric representation, but this is not required. A part may instantiate one or several children with a parental link.

Example : the DMU wheel is composed of the parts tire, rim, hub, bolt in the given instantiation chain :

  • wheel
    • tire with link wheel_tire-1
    • rim with link wheel_rim-1
      • hub with link rim_hub-1
        • bolt with link hub_bolt-generic
        • bolt with link hub_bolt-generic
        • bolt with link hub_bolt-generic
        • bolt with link hub_bolt-custom

Each part has common metadata shared by other instances of the same part, and each link has some metadata. The DMU provider has also attached the following instance metadata :

  • the instance referenced by wheel - tire - rim - hub - bolt at position X1,Y1,Z1 has the instance name bolt_1.
  • the instance referenced by wheel - tire - rim - hub - bolt at position X2,Y2,Z2 has the instance name bolt_2.
  • the instance referenced by wheel - tire - rim - hub - bolt at position X3,Y3,Z3 has the instance name bolt_3.

With this example we have introduced the three main metadata documents that compose a DMU :

  • the part metadata document (data shared by all instances of this element).
  • the link metadata document (the information included in each father/child relationship).
  • the instance metadata document (specific information bound to only specific instances and not all the instances of a given part).

As such, the bolts of the given DMU have the given documents

  • 1st bolt : part_metadata_bolt, link_metadata_hub_bolt-generic, instance_metadata_bolt-1
  • 2nd bolt : part_metadata_bolt, link_metadata_hub_bolt-generic, instance_metadata_bolt-2
  • 3rd bolt : part_metadata_bolt, link_metadata_hub_bolt-generic, instance_metadata_bolt-3
  • 4th bolt : part_metadata_bolt, link_metadata_hub_bolt-custom

Each document in the DMU has metadata (associations key : value) that enrich the instances that have it.

part_metadata_bolt document :

{ 
"id" : "part_metadata_bolt",
"Name" : "Wheel Bolt",
"Original Name" : "WHEEL-BOLT",
"PartNumber" : "WHEEL-BOLT",
"Diameter" : "10",
"Diameter Units" : "mm"
}

link_metadata_hub_bolt-generic document :

{ 
"id" : "link_metadata_hub_bolt-generic",
"Tightening Torque" : "80",
"Tightening Torque" : "N.m"
}

instance_metadata_bolt-1

{ 
"id" : "instance_metadata_bolt-1",
"Instance Name" : "bolt_1",
"S.N." : "70012341",
"Replacement Date" : "30/10/2022"
}

instance_metadata_bolt-2

{ 
"id": "instance_metadata_bolt-2",
"Instance Name" : "bolt_2",
"S.N." : "70034563",
"Replacement Date" : "30/01/2022"
}

instance_metadata_bolt-3

{ 
"id": "instance_metadata_bolt-3",
"Instance Name" : "bolt_3",
"S.N." : "70045675",
"Replacement Date" : "01/01/2021"
}

As such the bolt_1 has the metadata (i.e. the union of part_metadata_bolt, link_metadata_hub_bolt-generic, instance_metadata_bolt-1):

{ 
"Name" : "Wheel Bolt",
"Original Name" : "WHEEL-BOLT",
"PartNumber" : "WHEEL-BOLT",
"Diameter" : "10",
"Diameter Units" : "mm",
"Tightening Torque" : "80",
"Tightening Torque" : "N.m",
"Instance Name" : "bolt_1",
"S.N." : "70012341",
"Replacement Date" : "30/10/2022"
}

All metadata documents may be fetched by their id : an integer metadata document id that is recomputed on each build of the project. There may be attached documents (i.e. external documents) that are embedded or referenced (by using an URL) to part instances. All attached documents may be fetched by their id : a string attached document id.

Each document in the DMU has a string ID and an internal integer ID. Depending on the functions of the API, the API may expose integer or string based ids.

Security

The authorization system is based on tags that allow or disallow viewing some resources.

There exists two types of tags :

  • open tags that allow a user to open a Build.
  • extra tags inside a Build that allows multiple security levels inside a given build.

These tags may decorate individual documents (as seen previously) and 3D data inside a Build, adding increased security to these documents and each instance of the DMU may be decorated with documents of different types.

Each user is assigned a set of tags (UserData.viewtags that may be :

  • individually set for this user.
  • inherited from the user team(s) tag(s).
  • inherited from the web application being run at the moment.
  • other sources.

A user is allowed to open a build if its full list of the Build open tags are contained inside the Build.
NB: If a build is contained inside the ConnectionData, then the user has the required tags (Build.tags are only informational).

Intersecting the Build.extratags and UserData.viewtags tells the extra tags that may be used to see security protected data. These tags may be used to open a Build with fewer security levels, with DataSessionInterface.openDataSession.

Configurations

Each instantiation link in the DMU may be configured, i.e. has effectivities attached to it. An effectivity is a list of values that are acceptable for a given metadata name.

In the example below, the given instantiation link will be available for the configuration with model GT or the configuration with model ST AND trim special.

{
"effectivity": [
{
"model": "GT"
},
{
"model": "ST",
"trim": "special"
}
]
}

A configuration is a list of applicable effectivities. Only instantiation links with the same effectivity (or no effectivity i.e. unconfigured) will take part in the given configuration. A configuration is therefore a subset of the DMU with only the part instances that come from a matching instantiation link (the root part instance is included in all the configurations). Your DMU provider may have included configuration information in your DMU (optional).

Please see the samples in Working Sets.

Main ID Types

Each instance of the DMU is referred by a unique ID : the part instance id. This ID is not consistent from a build to the other, you MUST NOT rely on this number to be constant. Each new build of the same project involves a complete change of these ids.

Each geometry in the DMU has an ID : the geometric instance id.
That means that every single part instance that :

  • is a leaf
  • and has a geometrical representation

has a geometric instance id.

Each part instance that :

  • is not a leaf
  • has children (direct or not) with a graphical representation

is expressed with the list of geometric instance id of their children.

When a geometry is picked, the API gives the geometric instance id of the picked geometry.

There may be cases when 2 different part instances have the same geometric representation on the same location : in these cases, a single geometric instance id is the expression of two part instance ids.

The API in the Converters module allows to convert IDs depending on a Working set (see below).

In the same way, converting the root part instance to get all the geometric instance ids that are represented by it leads to getting all the geometric instance ids of the DMU.

There may be cases when two different part instance nodes (not leaves) have the same exact geometric representation at the same location. Usually, this concerns a single object in the DMU that fulfills two distinct functional usages. In such cases, the part instance nodes are considered different but instead of gathering all their children, taking their geometric instance id, removing duplicates, ordering them and comparing the results, such part instance nodes have the same class id. Each part instance is assigned a class id, and thus two different part instance nodes that have the same geometric representation have the same class id. Class ids are retrieved by an IdCardGetterInterface.

class ids example

Working Sets

All the interfaces exposed by the DataSessionInterface (actually in the Metadata namespace and its sub-namespaces) are used to make lists of part instance ids, in all the hierarchy (not only leaves, but nodes too). These interfaces usually expose a way to get the corresponding geometric instance ids.
The FilterItemInterface is the base interface of objects that express a filtering request (e.g. get part instances with name engine). Now, let us assume we make a FilterItemInterface in a WorkingSetInterface (see below) that provides a set with a single instance id : the root instance id. The corresponding geometric instance ids are therefore the geometric instance ids of all the DMU, since the root contains all leaves.

Creating a set of part instance ids is done with the use of a WorkingSetInterface. This interface allows to intersect filters (intersection, union, difference - or exclusion - : GroupOperator) content, configurations (more on this later) content and a list of other WorkingSetInterface content. One gets the result when the WorkingSetInterfaceSignal.WorkingSetReady signal is triggered. Depending on the filters in use, part instance leaves or nodes may be included in the resulting set. There is NO single treatment done on these part instance ids, it is up to the application, with the use of the optional InfiniteEngineInterface to make them ghosted/hidden ... (InfiniteEngineInterface.updateGeometricState and VisualStates) by getting their geometric instance ids.

Warning: the InfiniteEngineInterface only works with geometric instance ids.

The order of filters in the WorkingSetInterface is relevant and changing the order of the filters may (or not) change the final result. A GroupOperator is included in each FilterItemInterface : it represents the set operator to use when combined with the previous enabled filter in the list of the filters of the working set. As such, the GroupOperator of the first enabled FilterItemInterface in the list is always discarded (set as GroupOperator.GO_UNION), but if it is different that GroupOperator.GO_UNION, a warning is outputted in the console to help the user understand the final result.

The WorkingSetInterface content is accessible by:

  • a list of [geometric instance ids]
  • optionally a list of [part instance ids]
  • or nothing.

The way data is processed by the javascript api is governed by WorkingSetInterface.setRetrieveDataMode and the type of WorkingSetDataRetrieval. This property does not change the 'content' inside a WorkingSetInterface but the way data is available for applications, it is just a performance hint but that may dramatically reduce the amount of data exchanged from the server to the client.

The WorkingSetInterface may be provided with an optional list of WorkingSetInterface dependencies that may narrow the content of a given WorkingSetInterface, and dependencies are provided with an operator (GroupOperator). It represents the set operator to use when combined with the previous enabled WorkingSetInterface in the list of the dependencies. As such, the GroupOperator of the first enabled WorkingSetInterface in the list is always discarded (it is set as GroupOperator.GO_UNION).

WorkingSetInterface may be narrowed with a set of configurations.
The representation of a list of configurations represents the union of configurations (or unconfigured if the list of configurations is empty, meaning all the part instances in the DMU belong to this object).

As the WorkingSetInterface is the intersection of configurations, filters and other WorkingSetInterface, this implies that when any of filters, configurations, or dependencies are empty, then the geometric instance ids of such a WorkingSetInterface are empty.
In order to ease the use of WorkingSetInterface, the behavior of the WorkingSetInterface may be customized when any of these properties are empty with the enumeration WorkingSetBehavior.
The default behavior is WorkingSetBehavior.B_DefaultBehavior meaning that if any the filter, configuration, or dependencies is empty, then the WorkingSetInterface is empty.
On the contrary, with the WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit | WorkingSetBehavior.B_DiscardDependenciesIfEmptyBit behavior, then if no filters, no dependencies and no configurations are set, then the WorkingSetInterface is represented by ALL the geometric instance ids.
Please refer to WorkingSetBehavior for more information about the way data is computed inside a WorkingSetInterface.

Contrary to filters, the same WorkingSetInterface may be used in multiple WorkingSetInterface dependency lists.

Recommended usage:

It is highly advised to create specialized WorkingSetInterface to lower the bandwidth usage and correctly understand the way data is handled. It is advised to have at least one WorkingSetInterface that takes in charge the configurations (and ONLY configurations), using the behavior WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit | WorkingSetBehavior.B_DiscardDependenciesIfEmptyBit. Such a WorkingSetInterface will then act as a kind of 'Configuration context'. When you want to change the configurations of your visible items, you just change one WorkingSetInterface.
This behavior will also help with configured metadata. Indeed, the metadata of the DMU may be configured, meaning that the content of a specific attribute may depend on a 'chosen' configuration.

When an id-card request is made (more on this later), or a filter on a 'configured' attribute is computed, the given algorithm is used to get the 'current' configuration: The 'starting' WorkingSetInterface is the one that contains the given filter or is the target of the id-card request If the 'starting' WorkingSetInterface has some configurations set, then this list of configurations is used to compute the filter/id card request. If not, then all WorkingSetInterface dependencies that have not the operator GroupOperator.GO_EXCLUSION are crawled (with the exception of the first enabled dependency that is always crawled), incrementing the given WorkingSetInterface dependency depth by one. And this crawling is done recursively, leading to a list of WorkingSetInterface with a given depth (relative to the 'starting' WorkingSetInterface). This list is filtered with only WorkingSetInterface that have configurations. The WorkingSetInterface with the lowest depth is chosen, and in case of equality, the WorkingSetInterface that is included first in the dependency list is chosen. The list of configurations of this WorkingSetInterface is used to compute the id-card request, or filter.

/** 
* Sample to illustrate the use of an "unconfigured" configuration (all `part instances`) in the WorkingSetInterface.
*/
import { WorkingSetInterface, DataSessionInterface, WorkingSetDataRetrieval, WorkingSetBehavior } from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;
// create a WorkingSetInterface
// we want to get geometric instances result
// if nothing is set, this working set should include all parts ('unconfigured')
const lConfContext : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit | WorkingSetBehavior.B_DiscardDependenciesIfEmptyBit
);

// we will be 'unconfigured' for this context
// set the active configurations
lConfContext.setActiveConfs([]);
// and ask to compute all changes requested since the last call to the DataSessionInterface
lDataSession.update();

You may need to display only a subset of data depending on some criteria, e.g. the part instances of your DMU in the given 3D cube that has the field CompletionStatus equal to done. Such a list of filters is grouped in a WorkingSetInterface in the Metadata module. The following code snippet may be used to get the corresponding subset :

/** 
* Sample to illustrate the use of an intersection of
* FilterAABBInterface and FilterAttributeInterface.
*/
import {
AABB, AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
WorkingSetInterface,
FilterAABBInterface, FilterAttributeInterface, GroupOperator, WorkingSetDataRetrieval, WorkingSetBehavior,
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;

// MAKE SURE the attributes "CompletionStatus"
const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
const lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('CompletionStatus');
// make sure the attribute is a string one
console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

// create the resolution chain :
// create a Working set "unconfigured"
// first create WorkingSetInterface "unconfigured"
// we want to get geometric instances result
// if nothing is set, this working set should include all parts ('unconfigured')
const lConfCtx : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit | WorkingSetBehavior.B_DiscardDependenciesIfEmptyBit
);

// and create the Working set and bind it to the unconfigured WorkingSetInterface
const lConfVisibilityCtx: WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_Nothing,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit
);
lConfVisibilityCtx.insertWorkingSetDependency(0, lConfCtx, GroupOperator.GO_INTERSECTION);

// the AABB to use
const lABB : AABB = new AABB();
lABB.mCenter.x = 3;
lABB.mCenter.y = 3;
lABB.mCenter.z = 3;

lABB.mHalfExtent.x = 10;
lABB.mHalfExtent.y = 10;
lABB.mHalfExtent.z = 10;

// create a Filter Solver
const lFilterSolver : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit
);
// we want to get geometric instances result
lFilterSolver.setRetrieveDataMode(WorkingSetDataRetrieval.R_OnlyGeometricInstances);
// create a box filter
const lFilterAABB : FilterAABBInterface = lDataSession.createFilterAABB();
// useless, GroupOperator.GO_UNION is the default operator when creating a new filter
// lFilterAABB.setFilterOperator(GroupOperator.GO_UNION);

lFilterAABB.setAABB(lABB);

// create an FilterAttributeInterface
const lFilterAttributes : FilterAttributeInterface = lDataSession.createFilterAttribute();
// completion status to done
lFilterAttributes.setAttributeName('CompletionStatus');
lFilterAttributes.setExactValues(['done']);
// no n/a
lFilterAttributes.setNaValueChecked(false);

// intersection is the way to go since intersection of box and instances that have the field "CompletionStatus"
// to "done"
// only change the operator of the filters except the first
lFilterAttributes.setFilterOperator(GroupOperator.GO_INTERSECTION);

// and add the filters
// push back (-1) the AABB filter
lFilterSolver.insertFilter(-1, lFilterAABB);
// push back (-1) the FilterAttributeInterface, as it is the second one, its operator is used and therefore
// intersection is used
lFilterSolver.insertFilter(-1, lFilterAttributes);

// set the conf context to use => unconfigured for this example
lFilterSolver.insertWorkingSetDependency(0, lConfVisibilityCtx, GroupOperator.GO_UNION);
// and tell the DataSessionInterface to update the modified WorkingSetInterfaces
lDataSession.update();

There are cases when the user wants to get the list of part instance of a given filtering request. We assume that we want to :

  • display the part instances of your DMU in a given 3D cube that has the field CompletionStatus equal to done.
  • colorize in yellow the part instances that have the field ToBeRedesigned equal to true.
/** 
* Sample to illustrate the use of multiple WorkingSetInterfaces to define a set of visible pieces, and
* colorize a subset.
* The visibility is composed of all the part instances included in a Box, that have the "CompletionStatus" attribute
* set to "done".
* The colorization set is based on the visibility, the part instances that have the attribute "ToBeRedesigned"
* true should be colored in yellow.
* This colors all part instance in yellow in the given box, that ave the attribute "ToBeRedesigned"
* true and "CompletionStatus" attribute set to "done".
* This sample uses multiple WorkingSetInterface.
* It uses some FilterAABBInterface, FilterAttributeInterface, FilterBooleanInterface.
*/
import {
AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
InfiniteEngineInterface, Vector3, AABB, FilterAABBInterface, FilterAttributeInterface,
GroupOperator, VisualStates, FilterBooleanInterface, WorkingSetInterface, WorkingSetInterfaceSignal,
WorkingSetDataRetrieval,
WorkingSetBehavior
} from 'generated_files/documentation/appinfiniteapi';

// lEngineInterface has been created previously
let lEngineInterface : InfiniteEngineInterface;
// the DataSessionInterface has been created previously, is connected and is bound to the Infinite engine
let lDataSession : DataSessionInterface;

// MAKE SURE the attributes "CompletionStatus", "ToBeRedesigned" are relevant
const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('CompletionStatus');
// make sure the attribute is a string one
console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

lAttributeInfo = lAttributeDictionary.getAttributeInfo('ToBeRedesigned');
// make sure the attribute is a date one
console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_BOOLEAN));

// create a custom material yellow
const lMaterialId : number = lEngineInterface.getMaterialManager().createNewMaterial(new Vector3(1, 1, 0));
// create a a working set that contain the configurations
// we want to get geometric instances result
// if nothing is set, this working set should include all parts ('unconfigured')
const lConfContext : WorkingSetInterface = lDataSession.createWorkingSet(
// we may retrieve the geometric instances
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
// This working set should discard empty sets if no
// configurations, no filters, and no dependencies
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit | WorkingSetBehavior.B_DiscardDependenciesIfEmptyBit
);

// no configuration for the moment (all `part instances`)
// lConfContext holds all geometric instance ids
lConfContext.setActiveConfs([]);

// the AABB to use
const lABB : AABB = new AABB();
lABB.mCenter.x = 3;
lABB.mCenter.y = 3;
lABB.mCenter.z = 3;

lABB.mHalfExtent.x = 10;
lABB.mHalfExtent.y = 10;
lABB.mHalfExtent.z = 10;

// create the visibility Working set
// we want to get geometric instances result
const lVisibilityFilterSolver : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit
);

// create the inner Working set
const lInnerWorkingSet : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_Nothing,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit | WorkingSetBehavior.B_DiscardFiltersIfEmptyBit
);
lInnerWorkingSet.insertWorkingSetDependency(0, lConfContext, GroupOperator.GO_INTERSECTION);
lVisibilityFilterSolver.insertWorkingSetDependency(0, lInnerWorkingSet, GroupOperator.GO_UNION);

// create a box filter
const lFilterAABB : FilterAABBInterface = lDataSession.createFilterAABB();
// useless, GroupOperator.GO_UNION is the default operator when creating a new filter
// lFilterAABB.setFilterOperator(GroupOperator.GO_UNION);
lFilterAABB.setAABB(lABB);

// create a FilterAttributeInterface
const lFilterAttributes : FilterAttributeInterface = lDataSession.createFilterAttribute();
// completion status to done
lFilterAttributes.setAttributeName('CompletionStatus');
lFilterAttributes.setExactValues(['done']);
// no n/a
lFilterAttributes.setNaValueChecked(false);

// intersection is the way to go since intersection of box and instances that have the field "CompletionStatus"
// to "done"
// only change the operator of the filters except the first
lFilterAttributes.setFilterOperator(GroupOperator.GO_INTERSECTION);

// and add the filters
// push back (-1) the AABB filter
lVisibilityFilterSolver.insertFilter(-1, lFilterAABB);
// push back (-1) the FilterAttributeInterface, as it is the second one, its operator is used and therefore
// intersection is used
lVisibilityFilterSolver.insertFilter(-1, lFilterAttributes);

lVisibilityFilterSolver.addEventListener(WorkingSetInterfaceSignal.WorkingSetReady, () =>
{
const lGeometries : Uint32Array | undefined = lVisibilityFilterSolver.getGeometricInstanceIds();
if (lGeometries)
{
// set hidden and not ghosted for everyone
lEngineInterface.updateGeometricStateForAll(VisualStates.S_Ghost | VisualStates.S_Hidden, (~VisualStates.S_Ghost) | VisualStates.S_Hidden);
// and set not hidden for the given geometries
lEngineInterface.updateGeometricState(lGeometries, VisualStates.S_Hidden, ~VisualStates.S_Hidden);
}
});

// create the colorize Working Set
// we want to get geometric instances result
const lColorizeFilterSolver : WorkingSetInterface = lDataSession.createWorkingSet(
WorkingSetDataRetrieval.R_OnlyGeometricInstances,
WorkingSetBehavior.B_DiscardConfigurationsIfEmptyBit
);
// create an attribute filter
const lColorizeFilterAttributes : FilterBooleanInterface = lDataSession.createFilterBoolean();
// search ToBeRedesigned attribute
lColorizeFilterAttributes.setAttributeName('ToBeRedesigned');
// value should be true
lColorizeFilterAttributes.setBooleanValue(true);

// when the WorkingSetInterface is ready, colorize the given `geometric instances`
lColorizeFilterSolver.addEventListener(WorkingSetInterfaceSignal.WorkingSetReady, () =>
{
const lGeomIds : Uint32Array | undefined = lColorizeFilterSolver.getGeometricInstanceIds();
if (lGeomIds)
{
// change material for these elements
lEngineInterface.getMaterialManager().changeMaterialOfInstances(lGeomIds, lMaterialId);
}
});

// set the Working set of the given `part instances` list
lColorizeFilterSolver.insertWorkingSetDependency(0, lVisibilityFilterSolver, GroupOperator.GO_UNION);
// add the colorize filter
lColorizeFilterSolver.insertFilter(-1, lColorizeFilterAttributes);
// and tell the DataSessionInterface to update the modified WorkingSetInterfaces
lDataSession.update();
// theWorking set may then be used to restrict a search, or a metadata retrieval to the given Working set.

NB : Any change in a WorkingSetInterface and any Filter will be broadcasted and computed on the next call to DataSessionInterface.update call.

Please refer to the documentation for each class for more detailed explanations.

Available Filters

All filter interfaces extend the FilterItemInterface base interface. They are used in a WorkingSetInterface. A filter is a way to elect part instances from specific criteria (depending on the type of the filter).
The same filter cannot be used in multiple WorkingSetInterface, FilterSetInterface or FilterCompoundInterface, each filter can only be contained in one and only one "container".
All filters have a unique id (UUID), a type (FilterType) depending on the filter, an enabled state, an inverted state and a filter operator (GroupOperator).
Any disabled filter will not take part in the computation of the result of the WorkingSetInterface.
If a filter elects a set of part instances, then the same inverted filter will elects all other part instances leaves minus the one in the non inverted filter.
Warning : in a FilterCompoundInterface, the meaning of the inverted state is different.
The filter operator of a filter tells the way the part instances elected from this filter combine with the preceding filter (union, intersection, minus). The filter operator of the first enabled filter is always ignored, but a warning is outputted in the console when the GroupOperator is different that GroupOperator.GO_UNION. There are no operator precedence when combining filters, they are processed sequentially by their order in the container that holds them. There are cases when the user may want to combine filters in a certain order, the "parenthesis" system is provided by the FilterSetInterface interface.
Suppose we have a DMU with 3 part instances {a,b,c} and 3 filters A = { a, b, c }, B = { b, c } , C = { a, c }.

We want to calculate (C U A) - B = C U A - B = { a }:

/** 
* Sample to illustrate the operator precedence.
*/
import { FilterItemInterface, WorkingSetInterface, GroupOperator, DataSessionInterface } from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;
let lFilterSolver: WorkingSetInterface;
// A, B and C have been created previously
let A : FilterItemInterface;
let B : FilterItemInterface;
let C : FilterItemInterface;

// compute (C U A) - B = C U A - B

// it is better to modify the filters first and insert them after if possible (less signals sent)

// filter operator of C will be discarded, but a warning is outputted in the console if not GroupOperator.GO_UNION.
C.setFilterOperator(GroupOperator.GO_UNION);
// compute union and exclusion
A.setFilterOperator(GroupOperator.GO_UNION);
B.setFilterOperator(GroupOperator.GO_EXCLUSION);

// and insert filters
lFilterSolver.insertFilter(-1, C);
lFilterSolver.insertFilter(-1, A);
lFilterSolver.insertFilter(-1, B);

// and tell the DataSessionInterface to update the modified WorkingSetInterface
lDataSession.update();

We want to calculate C U (A - B) = { a, c }:

/** 
* Sample to illustrate the operator precedence.
*/
import { FilterItemInterface, WorkingSetInterface, GroupOperator, FilterSetInterface, DataSessionInterface } from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;

let lFilterSolver: WorkingSetInterface;
// A, B and C have been created previously
let A : FilterItemInterface;
let B : FilterItemInterface;
let C : FilterItemInterface;

// compute C U (A - B)
// we create the parenthesis with a FilterSetInterface
const lParenthesis : FilterSetInterface = lDataSession.createFilterSet();

// it is better to modify the filters first and insert them after if possible (less signals sent)

// filter operator of C will be discarded, but a warning is outputted in the console if not GroupOperator.GO_UNION.
C.setFilterOperator(GroupOperator.GO_UNION);
// filter operator of A will be discarded, but a warning is outputted in the console if not GroupOperator.GO_UNION.
A.setFilterOperator(GroupOperator.GO_UNION);

// compute union and exclusion
lParenthesis.setFilterOperator(GroupOperator.GO_UNION);
B.setFilterOperator(GroupOperator.GO_UNION);

// add filters in parenthesis
lParenthesis.insertFilter(-1, A);
lParenthesis.insertFilter(-1, B);

// and insert filters
lFilterSolver.insertFilter(-1, C);
lFilterSolver.insertFilter(-1, lParenthesis);

// and tell the DataSessionInterface to update the modified WorkingSetInterface
lDataSession.update();

Warning : using such a filter is very risky since part instance id are recomputed at each new 3dJuump Infinite build process. That means that creating such a filter on a build will be invalidated if a new build is done on your project. It is highly recommended to create your filters based on geometric values and metadata rather than their part instance id. Use extreme caution when using this type of filter and be sure you know what you are doing. Moreover, such a filter may consume more memory that a combination of other filters if a large number of part instance id is inside the filter. This kind of filter may thus break digital continuity.

  • FilterSetInterface (Group Filter)
    The filter allows to gather filters together and to create an operator precedence with GroupOperator.
    It is a sort of "parenthesis" filter (see above).

Warning : the maximum supported depth (i.e. nested FilterSetInterface) is 8.

Filters examples

In the following examples, the metadata assembly is as follows (dates are expressed as day/month/year):

metadata assembly example

In all the examples, the WorkingSetInterface creation are not explained, as it is explained in the previous section : Working Sets.

  1. The user wants to select electrical `part instances` whose replacement date is within the next 10 days (we assume we are the 26th of jan 2021). The expected result is [part instance 3].
    1. Create the two required filters in a FilterCompoundInterface and put them in intersection :

      /**
      * Sample to illustrate :
      * Select electrical `part instances` whose replacement date is within the next 10 days (we assume we are the 26th of jan 2021).
      * Test : create two filters in a FilterCompoundInterface and put them in intersection.
      */
      import {
      AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
      FilterCompoundInterface, FilterRangeItemInterface, WorkingSetInterface, FilterAttributeInterface,
      GroupOperator, FilterRangeInterface
      } from 'generated_files/documentation/appinfiniteapi';

      // the DataSessionInterface has been created previously and is connected
      let lDataSession : DataSessionInterface;

      // The Filter Solver has been created previously, its Working set has been set
      let lFilterSolver : WorkingSetInterface;

      // MAKE SURE the attributes "type" and "replacement" are relevant */
      const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
      let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('type');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('replacement');
      // make sure the attribute is a date one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_DATE));

      // create the electrical filter
      const lElectricalFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to electrical (exactly)
      lElectricalFilter.setAttributeName('type');
      lElectricalFilter.setExactValues(['electrical']);
      // the filter will be used inside a FilterCompoundInterface, N/A cannot be used (limitation on FilterCompoundInterface)
      // lElectricalFilter.setNaValueChecked(false);
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter
      // lElectricalFilter.setFilterOperator(GroupOperator.GO_UNION);

      const lReplacementFilter : FilterRangeInterface = lDataSession.createFilterRange();
      // work on "replacement" metadata
      lReplacementFilter.setAttributeName('replacement');
      // create a range
      {
      const lRepRange : FilterRangeItemInterface = lReplacementFilter.createFilterRangeItem();
      // we want to include the bounds in the query
      lRepRange.setIncludedLower(true);
      lRepRange.setIncludedUpper(true);
      const lCurrentTime : number = Date.now();
      const lCurrentTimePlus10Days : number = lCurrentTime + 10 * 24 * 3600 * 1000;
      // range between now and 10 days
      lRepRange.setLowerBound(lCurrentTime);
      lRepRange.setUpperBound(lCurrentTimePlus10Days);
      }
      // intersection is the way to go since intersection of type electrical and instances that should be replaced within the next 10 days
      lReplacementFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      const lCompoundFilter : FilterCompoundInterface = lDataSession.createFilterCompound();
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter
      // lCompoundFilter.setFilterOperator(GroupOperator.GO_UNION);

      // add the filters inside the FilterCompoundInterface
      // push back (-1) the electrical filter
      lCompoundFilter.insertFilter(-1, lElectricalFilter);
      // push back (-1) the replacement filter, as it is the second one, its operator is used and therefore
      // intersection is used
      lCompoundFilter.insertFilter(-1, lReplacementFilter);

      // and add the compound filter
      lFilterSolver.insertFilter(-1, lCompoundFilter);

      // and tell the DataSessionInterface to update the modified WorkingSetInterfaces
      lDataSession.update();

      This results in [] inside the result of the WorkingSetInterface.getPartInstanceIds. This is not the expected result since there is no single metadata document inside the example that contains both a type and replacement metadata. The type metadata is inherited from the hierarchy.

    2. Create the two required filters in a WorkingSetInterface and put them in intersection :

      /**
      * Sample to illustrate :
      * Select electrical `part instances` whose replacement date is within the next 10 days (we assume we are the 26th of jan 2021).
      * Test : create two filters directly in a WorkingSetInterface and put them in intersection.
      */
      import {
      AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
      FilterRangeItemInterface, WorkingSetInterface, FilterAttributeInterface, GroupOperator, FilterRangeInterface,
      } from 'generated_files/documentation/appinfiniteapi';

      // the DataSessionInterface has been created previously and is connected
      let lDataSession : DataSessionInterface;

      // The Filter Solver has been created previously, its Working set has been set
      let lFilterSolver : WorkingSetInterface;

      // MAKE SURE the attributes "type" and "replacement" are relevant */
      const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
      let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('type');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('replacement');
      // make sure the attribute is a date one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_DATE));

      // create the electrical filter
      const lElectricalFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to electrical (exactly)
      lElectricalFilter.setAttributeName('type');
      lElectricalFilter.setExactValues(['electrical']);
      // warning : n/a is set to true as default, we do not want to include other `part instances`
      lElectricalFilter.setNaValueChecked(false);
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter
      // lElectricalFilter.setFilterOperator(GroupOperator.GO_UNION);

      const lReplacementFilter : FilterRangeInterface = lDataSession.createFilterRange();
      // work on "replacement" metadata
      lReplacementFilter.setAttributeName('replacement');
      // create a range
      {
      const lRepRange : FilterRangeItemInterface = lReplacementFilter.createFilterRangeItem();
      // we want to include the bounds in the query
      lRepRange.setIncludedLower(true);
      lRepRange.setIncludedUpper(true);
      const lCurrentTime : number = Date.now();
      const lCurrentTimePlus10Days : number = lCurrentTime + 10 * 24 * 3600 * 1000;
      // range between now and 10 days
      lRepRange.setLowerBound(lCurrentTime);
      lRepRange.setUpperBound(lCurrentTimePlus10Days);
      }
      // intersection is the way to go since intersection of type electrical and instances that should be replaced within the next 10 days
      lReplacementFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      // and add the filters
      // push back (-1) the electrical filter
      lFilterSolver.insertFilter(-1, lElectricalFilter);
      // push back (-1) the replacement filter, as it is the second one, its operator is used and therefore
      // intersection is used
      lFilterSolver.insertFilter(-1, lReplacementFilter);

      // and tell the DataSessionInterface to update the modified WorkingSetInterfaces
      lDataSession.update();

      This results in [part instance 3] inside the result of the WorkingSetInterface.getPartInstanceIds. This is the expected result.

  2. The user wants to select electrical `part instances` which were plugged on the 01/01/2020. The expected result is [part instance 2].
    1. Create the three required filters in a FilterCompoundInterface and put them in intersection :

      /**
      * Sample to illustrate :
      * Select electrical `part instances` which were plugged on the 01/01/2020.
      * Test : create three filters in a FilterCompoundInterface and put them in intersection.
      */
      import {
      AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
      FilterCompoundInterface, FilterRangeItemInterface, WorkingSetInterface, FilterAttributeInterface,
      GroupOperator, FilterRangeInterface,
      } from 'generated_files/documentation/appinfiniteapi';

      // the DataSessionInterface has been created previously and is connected
      let lDataSession : DataSessionInterface;

      // The Filter Solver has been created previously, its Working set has been set
      let lFilterSolver : WorkingSetInterface;

      // MAKE SURE the attributes "type", "history.date" and "history.state" are relevant
      const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
      let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('type');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.date');
      // make sure the attribute is a date one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_DATE));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.state');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      // create the electrical filter
      const lElectricalFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to electrical (exactly)
      lElectricalFilter.setAttributeName('type');
      lElectricalFilter.setExactValues(['electrical']);
      // the filter will be used inside a FilterCompoundInterface, N/A cannot be used (limitation on FilterCompoundInterface)
      // lElectricalFilter.setNaValueChecked(false);
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter (and it will be the first one)
      // lElectricalFilter.setFilterOperator(GroupOperator.GO_UNION);

      const lDateFilter : FilterRangeInterface = lDataSession.createFilterRange();
      // work on "date" metadata
      lDateFilter.setAttributeName('history.date');
      // create a range
      {
      const lDateRange : FilterRangeItemInterface = lDateFilter.createFilterRangeItem();
      // we want to include the lower bound in the query, but exclude the upper bound (it is the next day !!!)
      lDateRange.setIncludedLower(true);
      lDateRange.setIncludedUpper(false);
      const lExpectedDate : number = Date.UTC(2020, 0, 1, 0, 0, 0, 0);
      const lExpiredDate : number = lExpectedDate + 24 * 3600 * 1000;

      // range between the 1st of january and the end of the first of january
      lDateRange.setLowerBound(lExpectedDate);
      lDateRange.setUpperBound(lExpiredDate);
      }
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lDateFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      // create the state filter
      const lStateFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to plugged (exactly)
      lStateFilter.setAttributeName('history.state');
      lStateFilter.setExactValues(['plugged']);
      // the filter will be used inside a FilterCompoundInterface, N/A cannot be used (limitation on FilterCompoundInterface)
      // lStateFilter.setNaValueChecked(false);
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lStateFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      const lCompoundFilter : FilterCompoundInterface = lDataSession.createFilterCompound();
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter
      // lCompoundFilter.setFilterOperator(GroupOperator.GO_UNION);

      // add the filters inside the FilterCompoundInterface
      // push back (-1) the electrical filter
      lCompoundFilter.insertFilter(-1, lElectricalFilter);
      // push back (-1) the date filter, as it is the second one, its operator is used and therefore
      // intersection is used
      lCompoundFilter.insertFilter(-1, lDateFilter);
      // push back (-1) the state filter, as it is the third one, its operator is used and therefore
      // intersection is used
      lCompoundFilter.insertFilter(-1, lStateFilter);

      // and add the compound filter
      lFilterSolver.insertFilter(-1, lCompoundFilter);

      // and tell the DataSessionInterface to update the modified WorkingSetInterfaces
      lDataSession.update();

      This results in [] inside the result of the WorkingSetInterface.getPartInstanceIds. This is not the expected result since there is no single metadata document inside the example that contains both type, history.state and history.date metadata. The type metadata is inherited from the hierarchy.

    2. Create the three required filters in a WorkingSetInterface and put them in intersection :

      /**
      * Sample to illustrate :
      * Select electrical `part instances` which were plugged on the 01/01/2020.
      * Test : create three filters directly in a WorkingSetInterface and put them in intersection.
      */
      import {
      AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
      FilterRangeItemInterface, FilterAttributeInterface, GroupOperator, FilterRangeInterface,
      WorkingSetInterface
      } from 'generated_files/documentation/appinfiniteapi';

      // the DataSessionInterface has been created previously and is connected
      let lDataSession : DataSessionInterface;

      // The Filter Solver has been created previously, its Working set has been set
      let lFilterSolver : WorkingSetInterface;

      // MAKE SURE the attributes "type", "history.date" and "history.state" are relevant
      const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
      let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('type');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.date');
      // make sure the attribute is a date one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_DATE));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.state');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      // create the electrical filter
      const lElectricalFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to electrical (exactly)
      lElectricalFilter.setAttributeName('type');
      lElectricalFilter.setExactValues(['electrical']);
      // warning : n/a is set to true as default, we do not want to include other `part instances`
      lElectricalFilter.setNaValueChecked(false);
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter (and it will be the first one)
      // lElectricalFilter.setFilterOperator(GroupOperator.GO_UNION);

      const lDateFilter : FilterRangeInterface = lDataSession.createFilterRange();
      // work on "date" metadata
      lDateFilter.setAttributeName('history.date');
      // create a range
      {
      const lDateRange : FilterRangeItemInterface = lDateFilter.createFilterRangeItem();
      // we want to include the lower bound in the query, but exclude the upper bound (it is the next day !!!)
      lDateRange.setIncludedLower(true);
      lDateRange.setIncludedUpper(false);
      const lExpectedDate : number = Date.UTC(2020, 0, 1, 0, 0, 0, 0);
      const lExpiredDate : number = lExpectedDate + 24 * 3600 * 1000;

      // range between the 1st of january and the end of the first of january
      lDateRange.setLowerBound(lExpectedDate);
      lDateRange.setUpperBound(lExpiredDate);
      }
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lDateFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      // create the state filter
      const lStateFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to plugged (exactly)
      lStateFilter.setAttributeName('history.state');
      lStateFilter.setExactValues(['plugged']);
      // warning : n/a is set to true as default, we do not want to include other `part instances`
      lStateFilter.setNaValueChecked(false);
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lStateFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      // add the filters inside the WorkingSetInterface
      // push back (-1) the electrical filter
      lFilterSolver.insertFilter(-1, lElectricalFilter);
      // push back (-1) the date filter, as it is the second one, its operator is used and therefore
      // intersection is used
      lFilterSolver.insertFilter(-1, lDateFilter);
      // push back (-1) the state filter, as it is the third one, its operator is used and therefore
      // intersection is used
      lFilterSolver.insertFilter(-1, lStateFilter);

      // and tell the DataSessionInterface to update the modified WorkingSetInterfaces
      lDataSession.update();

      This results in [part instance 2, part instance 3] inside the result of the WorkingSetInterface.getPartInstanceIds. This is not the expected result. Indeed, the part instance 3 is selected since instance document 1 has history.state : plugged and history.date : 01/01/2020. Even if the two metadata are not present in the same time in a metadata array, they can be both found on the metadata document. The FilterCompoundInterface can make sure (if the required values are contained inside an array) that the required values are found together in the same item in an array.

    3. Create the "history.date" and "history.state" filters in a FilterCompoundInterface, add it to a WorkingSetInterface with a "type" filter:

      /**
      * Sample to illustrate :
      * Select electrical `part instances` which were plugged on the 01/01/2020.
      * Test : Create "history.date" and "history.state" filters in a FilterCompoundInterface,
      * add to a WorkingSetInterface with a "type" filter.
      */
      import {
      AttributesDictionaryInterface, AttributeInfoInterface, AttributeType, DataSessionInterface,
      FilterCompoundInterface, FilterRangeItemInterface, WorkingSetInterface, FilterAttributeInterface,
      GroupOperator, FilterRangeInterface,
      } from 'generated_files/documentation/appinfiniteapi';

      // the DataSessionInterface has been created previously and is connected
      let lDataSession : DataSessionInterface;

      // The Filter Solver has been created previously, its Working set has been set
      let lFilterSolver : WorkingSetInterface;

      // MAKE SURE the attributes "type", "history.date" and "history.state" are relevant
      const lAttributeDictionary : AttributesDictionaryInterface = lDataSession.getAttributesDictionary();
      let lAttributeInfo : AttributeInfoInterface | undefined = lAttributeDictionary.getAttributeInfo('type');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.date');
      // make sure the attribute is a date one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_DATE));

      lAttributeInfo = lAttributeDictionary.getAttributeInfo('history.state');
      // make sure the attribute is a string one
      console.assert((lAttributeInfo !== undefined) && (lAttributeInfo.getAttributeType() === AttributeType.ATTR_STRING));

      // create the electrical filter
      const lElectricalFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to electrical (exactly)
      lElectricalFilter.setAttributeName('type');
      lElectricalFilter.setExactValues(['electrical']);
      // warning : n/a is set to true as default, we do not want to include other `part instances`
      lElectricalFilter.setNaValueChecked(false);
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter (and it will be the first one)
      // lElectricalFilter.setFilterOperator(GroupOperator.GO_UNION);

      const lDateFilter : FilterRangeInterface = lDataSession.createFilterRange();
      // work on "date" metadata
      lDateFilter.setAttributeName('history.date');
      // create a range
      {
      const lDateRange : FilterRangeItemInterface = lDateFilter.createFilterRangeItem();
      // we want to include the lower bound in the query, but exclude the upper bound (it is the next day !!!)
      lDateRange.setIncludedLower(true);
      lDateRange.setIncludedUpper(false);
      const lExpectedDate : number = Date.UTC(2020, 0, 1, 0, 0, 0, 0);
      const lExpiredDate : number = lExpectedDate + 24 * 3600 * 1000;

      // range between the 1st of january and the end of the first of january
      lDateRange.setLowerBound(lExpectedDate);
      lDateRange.setUpperBound(lExpiredDate);
      }
      // useless, GroupOperator.GO_UNION is the default operator when creating a new filter
      // lDateFilter.setFilterOperator(GroupOperator.GO_UNION);

      // create the state filter
      const lStateFilter : FilterAttributeInterface = lDataSession.createFilterAttribute();
      // type set to plugged (exactly)
      lStateFilter.setAttributeName('history.state');
      lStateFilter.setExactValues(['plugged']);
      // the filter will be used inside a FilterCompoundInterface, N/A cannot be used (limitation on FilterCompoundInterface)
      // lStateFilter.setNaValueChecked(false);
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lStateFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      const lCompoundFilter : FilterCompoundInterface = lDataSession.createFilterCompound();
      // intersection is the way to go since intersection of type electrical and instances with the given state at the given time
      lCompoundFilter.setFilterOperator(GroupOperator.GO_INTERSECTION);

      // add the filters inside the FilterCompoundInterface
      // push back (-1) the date filter,
      lCompoundFilter.insertFilter(-1, lDateFilter);
      // push back (-1) the state filter, as it is the second one, its operator is used and therefore
      // intersection is used
      lCompoundFilter.insertFilter(-1, lStateFilter);

      // and add the electrical and compound filter
      // push back (-1) the electrical filter
      lFilterSolver.insertFilter(-1, lElectricalFilter);
      lFilterSolver.insertFilter(-1, lCompoundFilter);

      // and tell the DataSessionInterface to update the modified WorkingSetInterfaces
      lDataSession.update();

      This results in [part instance 2] inside the result of the WorkingSetInterface.getPartInstanceIds. This is the expected result.

3dJuump Infinite Literal and Search query language

The query language follows the following BNF representation :

scopedClause     =	scopedClause boolean searchClause
                 |	searchClause
                 ;

searchClause     =	( scopedClause )
                 |	index comparator searchTerm
                 |	searchTerm
                 ;

comparator       =	comparatorSymbol
                 |	namedComparator
                 ;

searchTerm       =	term
                 ;

index            =	term
                 ;

term             =	identifier
                 ;

namedComparator  =	identifier
                 ;


identifier       =	[^\s/>=<()"]+
                 |	".*"
                 ;

comparatorSymbol =	=
                 |	>
                 |	<
                 |	>=
                 |	<=
                 |	<>
                 |	==
                 ;

boolean          =	and
                 |	or
                 |	not
                 ;

In the examples below, the test metadata document is as follows :

{
"metadata": {
"Issue": "A",
"PartNumber": "Freins Brembo",
"subsystem": "Sous caisse",
"Input Format and Emitter": "CATIA V5R19 SP0 HF0",
"Name": "Freins Brembo",
"filename": "Base Roulante/Sous caisse/Freins AV_brembo.CATPart",
"system": "Base Roulante",
"Last update": "31/03/2011",
"Design Status": "WIP already Released (Up Issue)",
"Design Name": "Freins AV-brembo",
"Original Name": "Freins Brembo",
"UUID": "0913b9260000032c504741b80000b3ae",
"OriginalUnits": {
"Original mass unit (kg)": 1,
"Original length unit (m)": 0.0010000000000000005,
"Original time unit (s)": 1
},
"mass" : 2
},
"type": "partmetadata",
"id": "xxx"
}

The query syntax follows the general rules below :

  • A word is a sequence of non blank characters. The separator is any blank character.
  • The search values are not sensitive to the case.
  • Date values are expressed as the number of milliseconds since January 1, 1970, 00:00:00 UTC.
  • Warning : a query that follows the grammar is not necessarily a valid query.
  • Truth values used for boolean type attributes are true and false, case insensitive.
  • There are special characters :
    • " is used to group words together and to include spaces in the clause (see identifier above in the grammar). "Freins Brembo" searches in all the metadata documents the words Freins and Brembo.
    • \ is the escape character in order to make a special character look normal (search explicitly " => \").
    • * character is the wildcard (see Wildcard character). The syntax is not the one of regular expression (i.e..*) but the one used for globing in file search command line tools. The wildcard represents any number of characters.
    • ? character represents exactly one arbitrary character.
    • $ character represents the start or end of a string. For example, :PartNumber = $Freins means to look for the metadata documents that have the field PartNumber that begins by Freins.
    • ( and ) are used to group clauses together and introduce operator precedence, for example (Freins or chassis) and issued tries to find all metadata documents that contains Freins or chassis in their content. Then, this set of metadata documents is reduced to get only metadata documents that contain also issued.
    • and, or, not are boolean binary operators. Warning : not is not an unary operator but a binary one (its meaning is actually "and not"). These operators serve to group clauses together.
    • : at the beginning of a word indicates a field in the metadata document : :PartNumber = Freins search for the word Freins in all the metadata documents that have the word Freins in the field PartNumber. The field is sensitive to the case. In order to search for values of the field Original length unit (m) : ":OriginalUnits.Original length unit (m)".
    • The operator =,>,<,>=,<=,<>,== are used to compare field content with values.
      • = is legal with string, number or date, or boolean metadata (not legal with date range or number range).
        • with string metadata, it is a loose equal comparator, it means the field contains. :PartNumber = Freins searches for the word Freins in all the metadata documents that have the metadata Freins in the field PartNumber.
        • In case of numeric metadata (date or number), that means the field value should be exactly the one provided.
        • In case of boolean metadata, that means the field value should be exactly the one provided, true or false.
      • >,>=,<,<= is legal with numeric and range (date, number, date range, number range) metadata. :mass > 2 : search all the metadata documents that have the metadata mass whose value is a number superior to 2.
      • <> is the not equal operator. It is legal with numeric (date, number) and boolean metadata. :mass <> 2 : search all the metadata documents that have the metadata mass whose value is not 2, but the field mass must be present in the metadata document.
      • == is the strict equal operator
        • with string metadata, the *, ? and $ cannot be used. The query is sensitive to the case and the field value should be exactly equal: :PartNumber == "Freins Brembo".
        • with other metadata, it is equivalent to the = operator : rules for = apply.

Some examples : The user wants to find metadata documents that contains :

  • Freins in the metadata document : "freins", the metadata document is matched.
  • Freins and caisse in the metadata document (not necessarily in the same field): "freins caisse" or "freins and caisse", the metadata document is matched.
  • Freins followed by Brembo in the same field (any field): "freins*brembo", the metadata document is matched.
  • Brembo followed by Freins in the same field (any field): brembo*freins, the metadata document is not matched.
  • Freins followed by Brembo with a single character in between in the same field (any field): freins?brembo, the metadata document is matched.
  • The metadata document whose PartNumber is exactly equal to Freins Brembo: :PartNumber == "Freins Brembo", the metadata document is matched.
  • The metadata documents whose PartNumber begins with Brembo: :PartNumber = $brembo, the metadata document is not matched.
  • The metadata documents whose PartNumber ends with Brembo: :PartNumber = brembo$, the metadata document is matched.

The 3dJuump Infinite Literal and Search query system works on metadata documents and not on the joined attribute set, a metadata document is matched if all the search clauses complies with this metadata document (and not on its hierarchy).

Search

A Search request is done through the SearchInterface interface. The search system operates on metadata documents (see Documents). The search result is therefore a list of metadata documents, their content and the resulting geometric instance ids (of all the metadata documents involved in the search result). The search query MUST be limited to a finite maximum number of metadata documents in order to monitor the bandwidth use. The maximum request number is 50. Also, the metadata content of the documents may be filtered to limit again the bandwidth use. The search query follows the 3dJuump Infinite Literal and Search query language. If the match query would get 80 metadata documents, but the request is limited to 40, the result will return the geometric instance ids from the part instances involved in the 80 metadata documents (and not the first 40 that are returned). The search request must be provided with a Working Sets, but the user may choose to limit the results to the Working set or on :

  • the whole DMU
  • Another WorkingSet that contain the first Working Sets

In that case the SearchDocumentResultInterface.isInWorkingSet function will tell if the given metadata document is included in a part instance in the Working Set, or the containing Working Set (this may be the whole DMU).

Be a source metadata document :

{
"metadata": {
"Issue": "A",
"PartNumber": "Freins Brembo",
"subsystem": "Sous caisse",
"Input Format and Emitter": "CATIA V5R19 SP0 HF0",
"Name": "Freins Brembo",
"filename": "Base Roulante/Sous caisse/Freins AV_brembo.CATPart",
"system": "Base Roulante",
"Last update": "31/03/2011",
"Design Status": "WIP already Released (Up Issue)",
"Design Name": "Freins AV-brembo",
"Original Name": "Freins Brembo",
"UUID": "0913b9260000032c504741b80000b3ae",
"OriginalUnits": {
"Original mass unit (kg)": 1,
"Original length unit (m)": 0.0010000000000000005,
"Original time unit (s)": 1
},
"mass" : 2
},
"type": "partmetadata",
"id": "xxx"
}

Calling

    // here the 'global' containing WorkingSetInterface is a WorkingSet that contain some configurations
search("\"freins brembo\"",lWorkingSet, false, 50, [], lConfContext);

will return the full document content.

Calling

    // here the 'global' containing WorkingSetInterface is 'undefined' meaning the whole DMU
search("\"freins brembo\"",lWorkingSet, false, 50, ["PartNumber"], undefined);

will return the following document:

{
"metadata": {
"PartNumber": "Freins Brembo"
},
"type": "partmetadata",
"id": "xxx"
}

The search mechanism is as follows :

/** 
* Sample to explain how to make search requests.
*/
import {
InfiniteEvent, SearchInterface, DocumentIdConverterInterface, WorkingSetInterface,
SearchDocumentResultInterface, DocumentIdResultInterface, DocumentIdConverterResultInterface,
SearchInterfaceSignal, DocumentIdConverterInterfaceSignal, DataSessionInterface, MetadataDocumentType
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;
// the Working set for the whole DMU as been set previously
let lCurrentDMUVisibilityCtx : WorkingSetInterface;

// the Working set for the search has been set previously (can be lCurrentDMUVisibilityCtx)
let lSearchVisibilityCtx : WorkingSetInterface;
// the string to search in documents
let lQueryString : string;
// the max number of results
const lMaxDocsToRetrieve : number = 50;

// create a search requester
const lSearch : SearchInterface = lDataSession.createSearch();

// to get actual results from a search request :
// the search requester gets result in the form of documents,
// in a second step, the user may enquire the server to know the
// relevant `part instance ids`
// the DocumentIdConverterInterface is there to make the conversion between
// a document id and the actual `part instances`
const lDocumentIdConverter : DocumentIdConverterInterface = lDataSession.createDocumentIdConverter();

// When the search is ready, we check for errors
// we may find all the `geometric instance ids` in the search result
// we get the first found document
// and find the relevant `part instance ids` and `geometric instance ids`
const onSearchReady = (pEvent: InfiniteEvent) : void =>
{
const lSearchInterface: SearchInterface = <SearchInterface>(pEvent.emitter);
// make sure the search in no more running
if (lSearchInterface.isRunning()) {
console.assert(false, 'this should not happen');
return;
}
// and make sure no error
if (lSearchInterface.getLastError() !== undefined) {
return;
}

// get all the `geometric instance ids` of the search request
const lGeometricInstanceIds: Uint32Array | undefined = lSearchInterface.getGeometricInstanceIds();
if (lGeometricInstanceIds !== undefined)
{
console.log('all the geometric instances concerned by the search:' + lGeometricInstanceIds.length);
}

// and get the documents (content is filtered by the last parameter of SearchInterface.search)
const lSearchResult: Array<SearchDocumentResultInterface> | undefined = lSearchInterface.getSearchDocuments();
if (lSearchResult) {
console.log('Search finished: ' + lSearchResult.length + ' result(s)');
// do we have all documents, or are there more that were not included due to the search results capped to lMaxDocsToRetrieve ?
if (lSearchInterface.hasOtherResults())
{
// some GUI code there perhaps ?
console.log('Search was capped to ' + lMaxDocsToRetrieve);
}
if (lSearchResult.length > 0) {
// get the `part instance ids` of the first document
const lDocumentId: number = lSearchResult[0].getDocumentId();
const lIsInVisibility : boolean = lSearchResult[0].isInWorkingSet();
const lDocumentType: MetadataDocumentType = lSearchResult[0].getDocumentType();
let lVisibilityCtxToUse : WorkingSetInterface;
if (lIsInVisibility)
{
console.log('retrieve document in visibility : ' + lDocumentId);
// we will try here to find instances only in the 'search' Working set
// but depending on your needs, you may use lCurrentDMUVisibilityCtx to extend the
// result to the `part instances` of the whole DMU, it is up to you
// or make 2 requests, one in the 'search' ctx, the other outside, or ....
lVisibilityCtxToUse = lSearchVisibilityCtx;
}
else {
// this case cannot happen if the third parameter to search is true
console.log('retrieve document outside visibility : ' + lDocumentId);
// in order to get results, we will make the conversion in the whole DMU Working set
// since this concerns `part instances` outside the 'search' Working set
lVisibilityCtxToUse = lCurrentDMUVisibilityCtx;
}
let lPartDocuments : Array<number> | undefined = undefined;
let lLinkDocuments : Array<number> | undefined = undefined;
let lInstanceDocuments : Array<number> | undefined = undefined;

// find the `part instance ids` and `geometric instance ids` of this document
switch(lDocumentType)
{
case MetadataDocumentType.MDT_PartMetadata:
lPartDocuments = [lDocumentId];
break;
case MetadataDocumentType.MDT_LinkMetadata:
lLinkDocuments = [lDocumentId];
break;
default:
lInstanceDocuments = [lDocumentId];
break;
}
// find the `part instance ids` and `geometric instance ids` of this document
lDocumentIdConverter.convert(lPartDocuments, lLinkDocuments, lInstanceDocuments, lVisibilityCtxToUse);
}
}
};

// When the document `part instances` are found
// we get the relevant `part instance ids` and `geometric instance ids` of the requested document
const onDocumentIdConverterReady = (pEvent: InfiniteEvent) : void =>
{
const lEventDocumentIdConverter: DocumentIdConverterInterface = <DocumentIdConverterInterface>(pEvent.emitter);
// make sure the search in no more running
if (lEventDocumentIdConverter.isRunning()) {
console.assert(false, 'this should not happen');
return;
}
if (lEventDocumentIdConverter.getLastError() !== undefined) {
return;
}
const lResult: Array<DocumentIdConverterResultInterface> | undefined = lEventDocumentIdConverter.getConversionResult();
if (lResult && lResult.length === 1)
{
const lDocumentIdConverterInstances: Array<DocumentIdResultInterface> = lResult[0].getConvertedInstances();
const lPartInstanceIds: Uint32Array = new Uint32Array(lDocumentIdConverterInstances.length);

for (let i = 0; i < lDocumentIdConverterInstances.length; i += 1) {
lPartInstanceIds[i] = lDocumentIdConverterInstances[i].getPartInstanceId();
}
console.log('documents converted :');
console.log('Convert part instances to geometric instance ids: ' + JSON.stringify(lPartInstanceIds));
}
};

// what to do when we get the results ?
lDocumentIdConverter.addEventListener(DocumentIdConverterInterfaceSignal.DocumentIdConverterReady, onDocumentIdConverterReady);
lSearch.addEventListener(SearchInterfaceSignal.SearchReady, onSearchReady);

// make a search request, and only retrieve the "PartNumber" metadata of the result documents
// we will get all the documents of the DMU (fifth parameter : lCurrentDMUVisibilityCtx), but will get a hint of the
// documents inside lSearchVisibilityCtx
// imit the results to lMaxDocsToRetrieve documents maximum
lSearch.search(lQueryString, lSearchVisibilityCtx, lMaxDocsToRetrieve, ['PartNumber'], lCurrentDMUVisibilityCtx);

or asynchronously :

/** 
* Sample to explain how to make asynchronous search requests.
*/
import {
SearchInterface, DocumentIdConverterInterface,
SearchDocumentResultInterface, DocumentIdResultInterface,
AsyncSearchResult, AsyncResultReason, AsyncSearchResultContent, AsyncDocumentIdConverterResultInterfaceResult,
DocumentIdConverterResultInterface, DataSessionInterface, MetadataDocumentType, WorkingSetInterface
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;
// the Working set for the whole DMU as been set previously
let lCurrentDMUVisibilityCtx : WorkingSetInterface;

// the Working set for the search has been set previously (can be lCurrentDMUVisibilityCtx)
let lSearchVisibilityCtx : WorkingSetInterface;
// the string to search in documents
let lQueryString : string;
// the max number of results
const lMaxDocsToRetrieve : number = 50;

// create a search requester
const lSearch : SearchInterface = lDataSession.createSearch();

// to get actual results from a search request :
// the search requester gets result in the form of documents,
// in a second step, the user may enquire the server to know the
// relevant `part instance ids`
// the DocumentIdConverterInterface is there to make the conversion between
// a document id and the actual `part instances`
const lDocumentIdConverter : DocumentIdConverterInterface = lDataSession.createDocumentIdConverter();

const triggerSearchRequest = async () : Promise<void> =>
{
// make a search request, and only retrieve the "PartNumber" metadata of the result documents
// we will get all the documents of the DMU (fifth parameter : lCurrentDMUVisibilityCtx), but will get a hint of the
// documents inside lSearchVisibilityCtx
// limit the results to lMaxDocsToRetrieve documents maximum
const lResult : AsyncSearchResult = await lSearch.asyncSearch(lQueryString, lSearchVisibilityCtx, lMaxDocsToRetrieve, ['PartNumber'], lCurrentDMUVisibilityCtx);

if (lResult.reason !== AsyncResultReason.ARR_Success || lResult.value === undefined)
{
console.assert(false, 'this should not happen');
return;
}
// When the search is ready, we check for errors
// we may find all the `geometric instance ids` in the search result
// we get the first found document
// and find the relevant `part instance ids` and `geometric instance ids`

const lSearchContent : AsyncSearchResultContent = lResult.value;
// get all the `geometric instance ids` of the search request
const lGeometricInstanceIds: Uint32Array | undefined = lSearchContent.geometricInstanceIds;
if (lGeometricInstanceIds !== undefined)
{
console.log('all the geometric instances concerned by the search:' + lGeometricInstanceIds.length);
}

// and get the documents (content is filtered by the last parameter of SearchInterface.search)
const lSearchResult: Array<SearchDocumentResultInterface> | undefined = lSearchContent.searchDocuments;
if (!lSearchResult) {
console.assert(false, 'this should not happen');
return;
}
console.log('Search finished: ' + lSearchResult.length + ' result(s)');
// do we have all documents, or are there more that were not included due to the search results capped to lMaxDocsToRetrieve ?
if (lSearchContent.hasOtherResults)
{
// some GUI code there perhaps ?
console.log('Search was capped to ' + lMaxDocsToRetrieve);
}
if (lSearchResult.length <= 0) {
console.log('no document found');
return;
}
// get the `part instance ids` of the first document
const lDocumentId: number = lSearchResult[0].getDocumentId();
const lIsInVisibility : boolean = lSearchResult[0].isInWorkingSet();
const lDocumentType: MetadataDocumentType = lSearchResult[0].getDocumentType();
let lVisibilityCtxToUse : WorkingSetInterface;
if (lIsInVisibility)
{
console.log('retrieve document in visibility : ' + lDocumentId);
// we will try here to find instances only in the search Working set
// but depending on your needs, you may use lCurrentDMUVisibilityCtx to extend the
// result to the `part instances` of the whole DMU, it is up to you
// or make 2 requests, one in the search ctx, the other outside, or ....
lVisibilityCtxToUse = lSearchVisibilityCtx;
}
else {
// this case cannot happen if the third parameter to search is true
console.log('retrieve document outside visibility : ' + lDocumentId);
// in order to get results, we will make the conversion in the whole DMU Working set
// since this concerns `part instances` outside the 'search' Working set
lVisibilityCtxToUse = lCurrentDMUVisibilityCtx;
}
let lPartDocuments : Array<number> | undefined = undefined;
let lLinkDocuments : Array<number> | undefined = undefined;
let lInstanceDocuments : Array<number> | undefined = undefined;

// find the `part instance ids` and `geometric instance ids` of this document
switch(lDocumentType)
{
case MetadataDocumentType.MDT_PartMetadata:
lPartDocuments = [lDocumentId];
break;
case MetadataDocumentType.MDT_LinkMetadata:
lLinkDocuments = [lDocumentId];
break;
default:
lInstanceDocuments = [lDocumentId];
break;
}
const lConversion : AsyncDocumentIdConverterResultInterfaceResult = await lDocumentIdConverter.asyncConvert(lPartDocuments, lLinkDocuments, lInstanceDocuments, lVisibilityCtxToUse);

// When the document `part instances` are found
// we get the relevant `part instance ids` and `geometric instance ids` of the requested document
if (lConversion.reason !== AsyncResultReason.ARR_Success || lConversion.value === undefined)
{
console.assert(false, 'this should not happen');
return;
}
const lResultArray : Array<DocumentIdConverterResultInterface> = lConversion.value;
if(lResultArray.length !== 1)
{
console.assert(false, 'this should not happen');
return;
}
const lDocumentIdConverterInstances: Array<DocumentIdResultInterface> = lResultArray[0].getConvertedInstances();
const lPartInstanceIds: Uint32Array = new Uint32Array(lDocumentIdConverterInstances.length);

for (let i = 0; i < lDocumentIdConverterInstances.length; i += 1) {
lPartInstanceIds[i] = lDocumentIdConverterInstances[i].getPartInstanceId();
}
console.log('documents converted :');
console.log('Convert part instances to geometric instance ids: ' + JSON.stringify(lPartInstanceIds));
};

triggerSearchRequest();

Annotations

The 3Djuump architecture allows to download and display 2D data representing measures, drawings (Functional Tolerancing and Annotations, Product Manufacturing Information, Measures, etc ...). These data are basically a 3D plane containing information in form of texts, curves, and symbols in this plane. Any data of this type is referred to an Annotation.
Annotations are only available from the DMU if your maintainer has included such data, future versions of the web API will allow creating custom annotations.

Annotations must be first downloaded, then displayed. Upon creation, they are affected an Annotation Id that allows them to be accessed, modified and queried. Each annotation has a display flag AnnotationInstanceState with a priority (to show an annotation on top of others regardless of their depth), a visible status (visible/hidden), and some display properties (show on top of geometries, highlighted, transparency if below geometries).

Annotations in the same plane and of the same meaning are grouped together in an Annotation View. Multiple Annotation Views may be grouped together in Annotation Groups. An id-card request provides access to Annotation Groups. An Annotation View features a display plane (position, orientation) and a number of Annotations. Annotation Views are accessed by an AnnotationView id. Annotation Views can be rotated, translated resulting in the transformation applied to all annotations contained within. Such a transformation is referred as the application matrix of the annotation view.

Retrieving annotations

Annotation Groups are available in an AnnotationGroupInfoInterface from a InstanceMetadataInterface by the InstanceMetadataInterface.getAnnotationGroups function (InstanceMetadataInterface from a PartInstanceInfoInterface are obtained by an id-card request with an IdCardGetterInterface). The AnnotationGroupInfoInterface allows to access the name of the Annotation view, and its type as defined by the DMU maintainer. All available annotation types are available through DataSessionInterface.getAnnotationTypes.

Once obtained, the Annotation view data is downloaded by an AnnotationGetterInterface that provides the result as an AnnotationResultInterface that contains :

  • The matrix to set to the annotations view (application matrix)
  • The AnnotationViewInterface that will be displayed.

Therefore, the AnnotationGetterInterface retrieves the content of the annotation view but also the matrix that places the instance to the correct location. This matrix also sets the offsets (application matrix) to set to accurately display the annotation view.

/** 
* Sample to illustrate the use of an AnnotationGetterInterface to retrieve multiple annotation views.
*/
import {
DataSessionInterface, IdCardGetterInterface, IdCardGetterInterfaceSignal, AnnotationGetterInterfaceSignal,
InfiniteEvent, PartInstanceInfoInterface, AnnotationGroupInfoInterface, AnnotationGetterInterface,
AnnotationResultInterface, AncestryInstanceMetadataInterface, WorkingSetInterface,
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession : DataSessionInterface;
// created previously
let lIdCardGetterInterface : IdCardGetterInterface;
// created previously, the working set to use for the id card query
let lWorkingSet : WorkingSetInterface;
// the part instance id to fetch
let lPartInstanceId : number;

// to retrieve the annotations
const lAnnotationViewGetter : AnnotationGetterInterface = lDataSession.createAnnotationGetter();

// what to do when we have retrieved id card information ?
let onIdCardReady : (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void;

// what to do when annotation data has been downloaded ?
let onAnnotationViewDownloaded : (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) => void;

// what to do when id card is ready ?
lIdCardGetterInterface.addEventListener(IdCardGetterInterfaceSignal.IdCardReady, onIdCardReady);
// what to do when the download of the annotation view is ready ?
lAnnotationViewGetter.addEventListener(AnnotationGetterInterfaceSignal.AnnotationFetchReady, onAnnotationViewDownloaded);

// onIdCardReady will be called when id-card data is available
onIdCardReady = (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
if (lIdCardGetterInterface.getLastError() !== undefined) {
// do nothing in case of error
// perhaps some GUI code ?
}
const lPartInstanceInfos : Array<PartInstanceInfoInterface> | undefined = lIdCardGetterInterface.getPartInstanceInfos();
if (!lPartInstanceInfos || lPartInstanceInfos.length !== 1)
{
// no data (isCancelled ?)
return;
}
// get the first the instantiation chain
const lCurrentChain : PartInstanceInfoInterface = lPartInstanceInfos[0];
// the annotation views that will be fetched
const lAnnotationsToFetch : Array<AnnotationGroupInfoInterface> = [];
// iterate over the metadata infos to retrieve all views at once
const lAllInstanceMetadata : Array<AncestryInstanceMetadataInterface> = lCurrentChain.getAncestorInstanceInfos();
// loops
let i : number;
let j : number;
// number of annotation views for this instance
let lNbAnnotationViews : number;
// number of ancestors
const lNbAncestors : number = lAllInstanceMetadata.length;
let lAnnotationViewsOfPartInstance :Array<AnnotationGroupInfoInterface>;
let lCurAnnotationView : AnnotationGroupInfoInterface;
for (i = 0; i < lNbAncestors; i += 1)
{
lAnnotationViewsOfPartInstance = lAllInstanceMetadata[i].getAnnotationGroups();
lNbAnnotationViews = lAnnotationViewsOfPartInstance.length;
for (j = 0; j < lNbAnnotationViews; j += 1)
{
lCurAnnotationView = lAnnotationViewsOfPartInstance[j];
// some fancy log
console.log('Will fetch annotation view ' + lCurAnnotationView.getGroupName() + ' of type ' + lCurAnnotationView.getGroupTypeName()
+ ' with ' + lCurAnnotationView.getAnnotationsCount() + ' annotations');
// add the annotation view to be fetched
lAnnotationsToFetch.push(lCurAnnotationView);
}
}
// and download
lAnnotationViewGetter.fetchAnnotationGroups(lAnnotationsToFetch);
};

// what to do when annotation data has been downloaded ?
onAnnotationViewDownloaded = (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// annotation are ready
const lAnnotationViewsContent : Array<AnnotationResultInterface> | undefined = lAnnotationViewGetter.getAnnotationGroupsResult();
if ((!lAnnotationViewsContent) || (lAnnotationViewsContent.length === 0))
{
console.log('error while fetching annotation content');
return;
}
// now annotation content is there, just need to display them
// use only the first view
const lCurAnnotationViewResult : AnnotationResultInterface = lAnnotationViewsContent[0];
console.log('display annotation view ' + lCurAnnotationViewResult.getGroupName() + ' (of type ' + lCurAnnotationViewResult.getGroupTypeName() + ')');
};

lIdCardGetterInterface.retrieveIdCard(lPartInstanceId, lWorkingSet);

Annotations positioning

Remember that each element in the DMU can be instantiated any number of times, thus Annotation views are usually designed in CAD software for a component in the local frame of the object. Each object is then instantiated at a particular location of the DMU. In order to be accurate, an annotation view for such a part must also be translated and oriented by the same amount. Such a matrix is referred as the application matrix of the annotation view.

The user may query the 3D plane orientation / position with the 4 vectors :

Or the user may query these information with a single call :

The Annotation View world matrix is the resulting matrix that moves the annotations to the expected location, but keep in mind that each annotation view has first a local frame (not alterable, part of the annotation view definition), and an application matrix. The Annotation View world matrix is the composition of the two matrices, the Annotation View local matrix multiplied by the Annotation View application matrix such that :

World = Application x Local

/** 
* Sample to illustrate the concept of annotation view matrices.
*/
import { tAnnotationViewId, Matrix4, InfiniteEngineInterface, AnnotationRendererInterface } from 'generated_files/documentation/appinfiniteapi';

// retrieved previously
let lAnnotationViewId : tAnnotationViewId;
// created previously
let lInfiniteEngine : InfiniteEngineInterface;
// retrieve the annotation renderer
const lAnnotationRenderer : AnnotationRendererInterface = lInfiniteEngine.getAnnotationRenderer();

// get the local matrix from the annotation view
const lLocalMatrix : Matrix4 = new Matrix4();
lAnnotationRenderer.getViewLocalMatrix(lAnnotationViewId, lLocalMatrix);

// get the application matrix from the annotation view
const lApplicationMatrix : Matrix4 = new Matrix4();
lAnnotationRenderer.getViewApplicationMatrix(lAnnotationViewId, lApplicationMatrix);

// get the world matrix from the annotation view
const lWorldMatrix : Matrix4 = new Matrix4();
lAnnotationRenderer.getViewWorldMatrix(lAnnotationViewId, lWorldMatrix);

// to store the computation of lApplicationMatrix * lLocalMatrix
const lResultingMatrix : Matrix4 = new Matrix4();

// compute res = lApplicationMatrix * lLocalMatrix
lLocalMatrix.multiplyMatrixLeft(lApplicationMatrix, lResultingMatrix);

// make sure the result is ok
const lExpectedResult : boolean = lWorldMatrix.equals(lResultingMatrix);
console.assert(lExpectedResult, 'the two matrices should be equal');

Annotations rendering state

The annotation rendering state is an "Or'ed" value of multiple flags.

First, each annotation is assigned a depth priority. There is a guarantee that an annotation with an higher priority will always be drawn on top of annotations with lower priority, regardless of their relative depth on screen. When two annotations have the same priority, then the one with the lower depth will be drawn on top of the other.

Then, each annotation has a rendering flag, visible (AnnotationInstanceState.AIS_Visible), overprint (AnnotationInstanceState.AIS_Overprint), highlighted (AnnotationInstanceState.AIS_Highlight), and appear through (AnnotationInstanceState.AIS_AppearThroughGeometry).

Any annotation that has not the visible flag is hidden.
Any annotation with the AnnotationInstanceState.AIS_Overprint flag is drawn on top of geometries, even if it should have been occluded behind.
Any annotation with the AnnotationInstanceState.AIS_Highlight flag is drawn with an orange pulse color.
Any annotation with the AnnotationInstanceState.AIS_AppearThroughGeometry flag is drawn on transparency if it is behind a geometry.

annotation rendering flags effect

All annotation flags are queried and set at once, with the use of a mask and a value. Any bit outside the mask will not be altered. Any bit inside the mask will take the bit value of the new value.

/** 
* Sample to illustrate the displaying of annotations.
*/
import { InfiniteEngineInterface, AnnotationRendererInterface, tAnnotationId, AnnotationInstanceState } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;
// retrieve the annotation renderer
const lAnnotationRenderer : AnnotationRendererInterface = lInfiniteEngine.getAnnotationRenderer();
// the annotation id to set
let lAnnotationId : tAnnotationId;

// the mask for the annotation
let lRenderingMask : number;
// the value for the annotation
let lRenderingState : number;

{
// we want to change all rendering flags
lRenderingMask = AnnotationInstanceState.AIS_AppearThroughGeometry | AnnotationInstanceState.AIS_Highlight | AnnotationInstanceState.AIS_Overprint | AnnotationInstanceState.AIS_Visible;
// and set the annotation only visible
lRenderingState = AnnotationInstanceState.AIS_Visible;
lAnnotationRenderer.setAnnotationRenderingState([lAnnotationId], lRenderingMask, lRenderingState);
}

{
// we want to change onl visibility
lRenderingMask = AnnotationInstanceState.AIS_Visible;
// and set the annotation only hidden
lRenderingState = 0;
lAnnotationRenderer.setAnnotationRenderingState([lAnnotationId], lRenderingMask, lRenderingState);
}
// etc ...

Displaying annotations

Once annotation views content are retrieved, the AnnotationViewInterface still need to be included in the 3D rendering to be parsed and set visible.
Each valid annotation view is assigned an AnnotationView id upon creation. Each valid annotation inside the annotation view is assigned an annotation id upon creation from AnnotationViewInterface.getFirstAnnotationId() to AnnotationViewInterface.getFirstAnnotationId() + AnnotationViewInterface.getAnnotationsCount() - 1.

The AnnotationRendererInterface is responsible for displaying annotation views.
Just call AnnotationRendererInterface.addAnnotationView with the retrieved AnnotationViewInterface (AnnotationResultInterface.getAnnotationViews), the correct matrix (AnnotationResultInterface.getGroupMatrix or AnnotationViewInterface.getDefaultMatrix) and the appropriate rendering flags (AnnotationInstanceState) (priority, visible status, show on top of geometries, highlighted, transparency if below geometries)).

The AnnotationRendererInterface.addAnnotationView function is asynchronous, it returns a request id. When the parsing of the annotation view is over, then an AnnotationRendererInterfaceSignal.AnnotationViewParsed signal is fired, with an AnnotationViewParsingResultInterface as attachment.

The AnnotationRendererInterface only works with AnnotationView ids and annotation ids.

Calling these methods will have the same output :

/** 
* Sample to illustrate the concept of annotation view ids and annotation ids.
*/
import { AnnotationRendererInterface, AnnotationViewInterface, AnnotationViewParsingResultInterface, InfiniteEngineInterface, tAnnotationViewId, Vector3 } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;

// obtained previously
let lParsingResult : AnnotationViewParsingResultInterface;
// lParsingResult :
// {
// "annotationViewId": 1,
// "annotationIdStart": 1,
// "nbAnnotations": 3,
// "requestId" : 1,
// "errorMessage" : ""
// }

const lAnnotationRenderer : AnnotationRendererInterface = lInfiniteEngine.getAnnotationRenderer();
let lExpectedResult : boolean;

// testing the world plane origin of the annotation view
{
const lPlaneOriginView : Vector3 = new Vector3();
const lPlaneOriginAnnotation : Vector3 = new Vector3();
const lAnnotationView : AnnotationViewInterface | undefined = lParsingResult.annotationView;
if(lAnnotationView === undefined)
{
throw new Error('the annotation view parsing failed');
}
// same result for world plane origin
// get world plane origin from view
lExpectedResult = lAnnotationRenderer.getViewWorldPlaneOrigin(lAnnotationView.getViewId(), lPlaneOriginView);
console.assert(lExpectedResult, 'this call should be valid');
// get world plane origin from the starting annotation of the view
lExpectedResult = lAnnotationRenderer.getAnnotationWorldPlaneOrigin(lAnnotationView.getFirstAnnotationId(), lPlaneOriginAnnotation);
console.assert(lExpectedResult, 'this call should be valid');
// the two vectors should be equal
console.assert(lPlaneOriginView.equals(lPlaneOriginAnnotation), 'the two vectors should be equal');

// get world plane origin from another annotation of the view (remember there are 3 annotations in this view)
lExpectedResult = lAnnotationRenderer.getAnnotationWorldPlaneOrigin(lAnnotationView.getFirstAnnotationId() + 1, lPlaneOriginAnnotation);
console.assert(lExpectedResult, 'this call should be valid');
console.assert(lPlaneOriginView.equals(lPlaneOriginAnnotation), 'the two vectors should be equal');

// get world plane origin from an invalid annotation (warning, if there are another annotation view lParsingResult.annotationIdStart+lParsingResult.nbAnnotations
// may be the starting annotation id of another annotation view)
lExpectedResult = lAnnotationRenderer.getAnnotationWorldPlaneOrigin(lAnnotationView.getFirstAnnotationId() + lAnnotationView.getAnnotationsCount(), lPlaneOriginAnnotation);
const lViewId : tAnnotationViewId = lAnnotationRenderer.getAnnotationViewId(lAnnotationView.getFirstAnnotationId() + lAnnotationView.getAnnotationsCount());
console.assert((!lExpectedResult) || (lViewId !== lAnnotationView.getViewId()), 'this call should not be valid');
}

// we can do exactly the same tests with :
// getViewWorldPlaneNormal / getAnnotationWorldPlaneNormal
// getViewWorldPlaneXAxis / getAnnotationWorldPlaneXAxis
// getViewWorldPlaneYAxis / getAnnotationWorldPlaneYAxis
// getViewApplicationMatrix / getAnnotationApplicationMatrix
// getViewWorldMatrix / getAnnotationWorldMatrix

And the final rendering is obtained through :

/** 
* Sample to illustrate the displaying of annotations.
*/
import {
AnnotationGetterInterface, AnnotationGetterInterfaceSignal, InfiniteEvent,
AnnotationResultInterface, InfiniteEngineInterface, AnnotationRendererInterface, AnnotationRendererInterfaceSignal,
tAnnotationViewId, AnnotationInstanceState, AnnotationViewParsingResultInterface, AnnotationViewInterface
} from 'generated_files/documentation/appinfiniteapi';

// created previously
let lAnnotationViewGetter : AnnotationGetterInterface;
// created previously
let lInfiniteEngine : InfiniteEngineInterface;
// retrieve the annotation renderer
const lAnnotationRenderer : AnnotationRendererInterface = lInfiniteEngine.getAnnotationRenderer();

// the annotation view id that has just been parsed
let lAnnotationViewId : tAnnotationViewId = -1;

// what to do when an annotation has been downloaded ?
let onAnnotationRetrieved : (pEvent: InfiniteEvent, pCallbackData: Object | undefined) => void;

// what to do when an annotation is ready to be displayed ?
let onAnnotationReady : (pEvent: InfiniteEvent, pCallbackData: Object | undefined) => void;

// connect to signals
lAnnotationViewGetter.addEventListener(AnnotationGetterInterfaceSignal.AnnotationFetchReady, onAnnotationRetrieved);
lAnnotationRenderer.addEventListener(AnnotationRendererInterfaceSignal.AnnotationViewParsed, onAnnotationReady);

// what to do when an annotation has been downloaded ?
onAnnotationRetrieved = (_pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// the result of the download
const lAnnotationViewsContent : Array<AnnotationResultInterface> | undefined = lAnnotationViewGetter.getAnnotationGroupsResult();
if (!lAnnotationViewsContent || (lAnnotationViewsContent.length === 0))
{
console.log('Weird, annotation is downloaded but no content');
return;
}
if (lAnnotationViewsContent.length > 1)
{
console.log('Only displaying one annotation view at the moment, other annotation views will be discarded');
return;
}
// take the first annotation result, discarding others
const lAnnotationResult : AnnotationResultInterface = lAnnotationViewsContent[0];

// and add the annotation view to be displayed with standard depth
// we want to set the depth and visible state of the annotation
// mask is AnnotationInstanceState.AIS_DepthPriorityMask | AnnotationInstanceState.AIS_Visible
// but we want to be depth std and invisible
// state is AnnotationInstanceState.AIS_DepthPriorityStd | 0 = AnnotationInstanceState.AIS_DepthPriorityStd
const lAnnotationViews : Array<AnnotationViewInterface> = lAnnotationResult.getAnnotationViews();
if(lAnnotationViews.length === 0)
{
console.log('Weird, annotation is downloaded but no result');
return;
}
const lMask : number = AnnotationInstanceState.AIS_DepthPriorityMask | AnnotationInstanceState.AIS_Visible;
const lState : number = AnnotationInstanceState.AIS_DepthPriorityStd;
const lRequestId : number = lAnnotationRenderer.addAnnotationView(lAnnotationViews[0], lAnnotationResult.getGroupMatrix(), lMask, lState);
if (lRequestId < 0)
{
// weird error
console.log('The parsing procedure could not be started');
}
};

// what to do when an annotation is ready to be displayed ?
onAnnotationReady = (pEvent: InfiniteEvent, _pCallbackData: Object | undefined) : void =>
{
// fancy log
console.log('annotation view has finished parsing');

// take the parsing result
const lParsingResult : AnnotationViewParsingResultInterface = pEvent.attachments;
if (lParsingResult.annotationView === undefined)
{
// this is an error
console.log('annotation view parsing has failed ' + JSON.stringify(lParsingResult.error));
return;
}

// store the annotation view id
lAnnotationViewId = lParsingResult.annotationView.getViewId();
const lNbAnnotations : number = lParsingResult.annotationView.getAnnotationsCount();
const lAnnotationStartId : number = lParsingResult.annotationView.getFirstAnnotationId();
// create the list of annotation ids from lParsingResult.annotationIdStart to lParsingResult.annotationIdStart + lParsingResult.nbAnnotations - 1
const lAnnotationIds : number [] = [];
lAnnotationIds.length = lNbAnnotations;
let i : number;
for (i = 0; i < lNbAnnotations; i += 1)
{
lAnnotationIds[i] = lAnnotationStartId + i;
}
// mask for all rendering flags (except priority)
const lMask : number = AnnotationInstanceState.AIS_AppearThroughGeometry | AnnotationInstanceState.AIS_Highlight
| AnnotationInstanceState.AIS_Overprint | AnnotationInstanceState.AIS_Visible;
// state is visible (but we may add AnnotationInstanceState.AIS_AppearThroughGeometry just for fun)
const lState : number = AnnotationInstanceState.AIS_Visible; // | AnnotationInstanceState.AIS_AppearThroughGeometry

lAnnotationRenderer.setAnnotationRenderingState(lAnnotationIds, lMask, lState);
// you should now see the annotation on the next rendering
};

or asynchronously :

/** 
* Sample to illustrate the asynchronous displaying of annotations.
*/
import {
AnnotationGetterInterface,
AnnotationResultInterface, AnnotationGroupInfoInterface, InfiniteEngineInterface, AnnotationRendererInterface,
tAnnotationViewId, AnnotationInstanceState, AnnotationViewParsingResultInterface, AsyncAnnotationResult, AsyncResultReason,
AnnotationViewInterface,
AsyncAnnotationViewParsingResult
} from 'generated_files/documentation/appinfiniteapi';

// created previously
let lAnnotationViewGetter : AnnotationGetterInterface;
// created previously
let lInfiniteEngine : InfiniteEngineInterface;
// retrieve the annotation renderer
const lAnnotationRenderer : AnnotationRendererInterface = lInfiniteEngine.getAnnotationRenderer();
// the annotation to fetch
let lAnnotationToFetch : AnnotationGroupInfoInterface;

// the annotation view id that has just been parsed
let lAnnotationViewId : tAnnotationViewId = -1;

const displayResult = async () : Promise<void> =>
{
const lAsyncAnnotationResult : AsyncAnnotationResult = await lAnnotationViewGetter.asyncFetchAnnotationGroups(lAnnotationToFetch);

// the result of the download
const lAnnotationViewsContent : Array<AnnotationResultInterface> | undefined = lAsyncAnnotationResult.value;
if (!lAnnotationViewsContent || (lAnnotationViewsContent.length === 0))
{
console.log('Weird, annotation download error');
return;
}
if (lAnnotationViewsContent.length > 1)
{
console.log('Weird, annotation is downloaded but too many results');
return;
}
// take the first annotation result, discarding others
const lAnnotationResult : AnnotationResultInterface = lAnnotationViewsContent[0];

// and add the annotation view to be displayed with standard depth
// we want to set the depth and visible state of the annotation
// mask is AnnotationInstanceState.AIS_DepthPriorityMask | AnnotationInstanceState.AIS_Visible
// but we want to be depth std and invisible
// state is AnnotationInstanceState.AIS_DepthPriorityStd | 0 = AnnotationInstanceState.AIS_DepthPriorityStd
let lMask : number = AnnotationInstanceState.AIS_DepthPriorityMask | AnnotationInstanceState.AIS_Visible;
let lState : number = AnnotationInstanceState.AIS_DepthPriorityStd;
const lAnnotationViews : Array<AnnotationViewInterface> = lAnnotationResult.getAnnotationViews();
if(lAnnotationViews.length === 0)
{
console.log('Weird, annotation is downloaded but no result');
return;
}
const lAnnotationParsingResult : AsyncAnnotationViewParsingResult = await lAnnotationRenderer.asyncAddAnnotationView(lAnnotationViews[0], lAnnotationResult.getGroupMatrix(), lMask, lState);

if (lAnnotationParsingResult.reason !== AsyncResultReason.ARR_Success
|| lAnnotationParsingResult.value === undefined)
{
// weird error
console.log('The parsing procedure could not be started');
return;
}
// fancy log
console.log('annotation view has finished parsing');

// take the parsing result
const lParsingResult : AnnotationViewParsingResultInterface = lAnnotationParsingResult.value;
if (lParsingResult.annotationView === undefined)
{
// this is an error
console.log('annotation view parsing has failed ' + JSON.stringify(lParsingResult.error));
return;
}

// store the annotation view id
lAnnotationViewId = lParsingResult.annotationView.getViewId();
const lNbAnnotations : number = lParsingResult.annotationView.getAnnotationsCount();
const lAnnotationStartId : number = lParsingResult.annotationView.getFirstAnnotationId();
// create the list of annotation ids from lParsingResult.annotationIdStart to lParsingResult.annotationIdStart + lParsingResult.nbAnnotations - 1
const lAnnotationIds : number [] = [];
lAnnotationIds.length = lNbAnnotations;
let i : number;
for (i = 0; i < lNbAnnotations; i += 1)
{
lAnnotationIds[i] = lAnnotationStartId + i;
}
// mask for all rendering flags (except priority)
lMask = AnnotationInstanceState.AIS_AppearThroughGeometry | AnnotationInstanceState.AIS_Highlight
| AnnotationInstanceState.AIS_Overprint | AnnotationInstanceState.AIS_Visible;
// state is visible (but we may add AnnotationInstanceState.AIS_AppearThroughGeometry just for fun)
lState = AnnotationInstanceState.AIS_Visible; // | AnnotationInstanceState.AIS_AppearThroughGeometry

lAnnotationRenderer.setAnnotationRenderingState(lAnnotationIds, lMask, lState);
// you should now see the annotations on the next rendering
};

displayResult();

Fonts

The 3djuump javascript api also bundles a fixed custom font, which is used as a default font. Annotations usually use different fonts (symbol fonts, arial, bold fonts, etc ...) but the AnnotationRendererInterface needs to have access to font definitions (curves, sizes, etc ...) which are not available as default in plain javascript apis. The AnnotationRendererInterface uses internally a library (opentype.js) that needs to have access to true type font files (usually files with .ttf extension). Such files must be registered onto the annotation rendering mechanism with the FontLoaderInterface before using AnnotationRendererInterface.addAnnotationView. As fonts are system wide, the FontLoaderInterface is a singleton (which is evil in normal cases) and is accessed through the InfiniteApiControllerInterface.

/** 
* Sample to illustrate the use of an FontLoaderInterface to register fonts.
*/
import { FontLoaderInterface, InfiniteFactory } from 'generated_files/documentation/appinfiniteapi';

// access the FontLoaderInterface singleton from the InfiniteFactory
const lFontLoader : FontLoaderInterface = InfiniteFactory.GetInfiniteApiController().getFontLoader();

// register arial regular which is on the same site of the main page
// name bold ? italic ? url
lFontLoader.registerFont('arial', false, false, './resources/fonts/arial.ttf');

// register arial bold which is on the same site of the main page
// name bold ? italic ? url
lFontLoader.registerFont('arial', true, false, './resources/fonts/arialbd.ttf');

Features

Starting with the 4.1 version of the api, typical features of 3D geometries are now computed and available in the api. features consist in lines, circles and patch borders (created with a given CAD model designing tool). The patch borders may now be displayed with InfiniteEngineInterface.enablePatchBorders or InfiniteEngineInterface.setPatchBordersEnabled.

patch borders rendering

Features are now accessed with the FeatureManagerInterface obtained through the InfiniteEngineInterface.getFeatureManager. Features of specific geometric instances may be displayed (FeatureManagerInterface.setGeometricInstancesFeaturesVisible, FeatureManagerInterface.setGeometricInstanceFeaturesVisible) and then are available in picking requests (more on this later). Displayed features are hover-able and the FeatureManagerInterfaceSignal.FeatureHoverChanged is sent whenever a feature under the cursor changes. Custom application features may also be created and displayed.

geometric `feature` display
/** 
* Sample to illustrate the showing of the circle `features` of some geometric instances.
*/
import { FeatureManagerInterface, FeatureVisibilityMode, InfiniteEngineInterface } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;

// the geometric instances to show `features`
let lGeometricInstanceIds : Array<number> | Uint32Array;

// retrieve the `feature` manager
const lFeatureManager : FeatureManagerInterface = lInfiniteEngine.getFeatureManager();

// display only circles of geometries
lFeatureManager.setGeometricInstancesFeaturesTypesVisibility(FeatureVisibilityMode.FVM_ArcOfCircle);

// hide all previously displayed `features`
lFeatureManager.clearGeometricInstanceFeaturesVisible();

// and show the `features` of the given geometries
lFeatureManager.setGeometricInstancesFeaturesVisible(lGeometricInstanceIds, true);

Feature types

Available features:

  • ArcOfCircleFeature : an arc of circle (may also be a full circle) with a center, radius, normal, angle and start direction. Arc of circle are detected inside the 3D geometries of the DMU. These features are drawn and can be picked.
  • LineFeature : a line with two extent points. Lines are detected inside the 3D geometries of the DMU. These features are drawn and can be picked.
  • PointFeature : a point with an optional normal if the point was picked on a surface. These features are drawn and can be picked.
  • OOBBFeature : an Object Oriented Bounding Box (OOBB). These features are drawn and can be picked.
  • PlaneFeature : an plane with a point and a normal. These features are not drawn and thus cannot be picked, they are only available for measurements.
  • GeometryFeature : a feature with a geometric instance id that represents a 3D geometry. These features are not drawn and thus cannot be picked, they are only available for measurements.

OOBB

Object oriented Bounding Boxes of 3D geometries are available through FeatureManagerInterface.retrieveOOBB and FeatureManagerInterface.asyncRetrieveOOBB. OOBB retrieval is asynchronous and the FeatureManagerInterfaceSignal.OOBBReady signal is sent when an OOBB request has completed. Object Oriented Bounding Boxes (OOBB) are similar to Axis Aligned Bounding Boxes (AABB) but with 3 axes that may not be axis aligned. OOBB are a lot more precise that AABB.

/** 
* Sample to illustrate the retrieval of the OOBB of a geometry.
*/
import { FeatureManagerInterface, FeatureManagerInterfaceSignal, InfiniteEngineInterface, InfiniteEvent, OOBB, OOBBTypeAttachment } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;

// the geometric instance to retrieve OOBB from
let lGeometricInstanceId : number;

// retrieve the `feature` manager
const lFeatureManager : FeatureManagerInterface = lInfiniteEngine.getFeatureManager();
// the OOBB request id
let lRetrieveOobbRequestId : number = 0;

// the resulting OOBB
let lOobbResult : OOBB | undefined = undefined;

// the callback called when a OOBB result is ready
const onOobbRetrieved = (pEvent: InfiniteEvent) : void =>
{
const lOOBBAttachment : OOBBTypeAttachment | undefined = pEvent.attachments;
// is this event valid ?
if(lOOBBAttachment === undefined || lOOBBAttachment.oobb === undefined)
{
console.log('invalid oobb attachment');
return;
}
// is this the correct request ?
if(lOOBBAttachment.oobbRequestId !== lRetrieveOobbRequestId)
{
//
return;
}
// finally, we made it !
lOobbResult = lOOBBAttachment.oobb;
console.log('Found oobb', JSON.stringify(lOobbResult));
const lApplicationId : number = lFeatureManager.createOOBBApplicationFeature(lOobbResult);
if(lApplicationId <= 0)
{
console.log('feature creation failed');
}
};

// register our callback
lFeatureManager.addEventListener(FeatureManagerInterfaceSignal.OOBBReady, onOobbRetrieved);

// and request an oobb
lRetrieveOobbRequestId = lFeatureManager.retrieveOOBB(lGeometricInstanceId);
if(lRetrieveOobbRequestId <= 0)
{
console.log('Failed to query oobb, is the geometric instance id correct ?');
}

Measurements

The 3djuump infinite api since the 4.1 version allows to perform measurements (i.e. minimal distance) between features. Measurements are performed by the FeatureManagerInterface obtained through the InfiniteEngineInterface.getFeatureManager. Measurements may be performed between points of the features or the center of the features (see MeasurementType). A measurement result consists in 2 contact points, one for each feature.

/** 
* Sample to illustrate the measurement between a geometry and an arc of circle.
*/
import {
AABB, ArcOfCircleFeature, DataSessionInterface, FeatureManagerInterface, FeatureManagerInterfaceSignal,
FeatureType, GeometryFeature, InfiniteEngineInterface, InfiniteEvent, MeasurementType, MeasurementTypeAttachment,
Unit, Vector3
} from 'generated_files/documentation/appinfiniteapi';

// the data session
// created previously
let lDataSession : DataSessionInterface;

// created previously
let lInfiniteEngine : InfiniteEngineInterface;

// the geometric instance to compute measurement from
let lGeometricInstanceId : number;

// retrieve the `feature` manager
const lFeatureManager : FeatureManagerInterface = lInfiniteEngine.getFeatureManager();
// the measurement request id
let lMeasurementRequestId : number = 0;

// the callback called when a measurement result is ready
const onMeasurementDone = (pEvent: InfiniteEvent) : void =>
{
const lMeasurementAttachment : MeasurementTypeAttachment | undefined = pEvent.attachments;
// is this event valid ?
if(lMeasurementAttachment === undefined || lMeasurementAttachment.contactPoint0 === undefined)
{
console.log('invalid measurement attachment');
return;
}
// is this the correct request ?
if(lMeasurementAttachment.measurementRequestId !== lMeasurementRequestId)
{
// bail out
return;
}
// finally, we made it !
console.log('Found 2 contact points', JSON.stringify(lMeasurementAttachment.contactPoint0), JSON.stringify(lMeasurementAttachment.contactPoint1));

// make two fancy points on the rendering
let lApplicationId : number = lFeatureManager.createPointApplicationFeature(lMeasurementAttachment.contactPoint0);
if(lApplicationId <= 0)
{
console.log('feature creation failed');
}
lApplicationId = lFeatureManager.createPointApplicationFeature(lMeasurementAttachment.contactPoint1);
if(lApplicationId <= 0)
{
console.log('feature creation failed');
}
// and show the result, we are here for that
console.log('Measurement distance', lMeasurementAttachment.contactPoint0.distanceToVector(lMeasurementAttachment.contactPoint1));
};

// register our callback
lFeatureManager.addEventListener(FeatureManagerInterfaceSignal.MeasurementReady, onMeasurementDone);

// retrieve the DMU center
const lDmuAABB : AABB = new AABB();
lDataSession.getDmuAABB(lDmuAABB);

// retrieve the up and front vectors of the scene
const lDmuUpVector : Vector3 = new Vector3();
const lDmuFrontVector : Vector3 = new Vector3();
lInfiniteEngine.getCameraManager().getFrameReference(lDmuFrontVector, lDmuUpVector);

// the arc of circle radius in meters
const lCircleFeatureRadiusInMeters : number = 1.3;

// first `feature` is a geometry
const lFirstFeature : GeometryFeature = {
type: FeatureType.FT_Geometry,
geometricInstanceId: lGeometricInstanceId
};

// second `feature` is an arc of circle
const lSecondFeature : ArcOfCircleFeature = {
type: FeatureType.FT_ArcOfCircle,
center: lDmuAABB.mCenter,
normal: lDmuUpVector,
startDirection: lDmuFrontVector,
radius: lCircleFeatureRadiusInMeters * lDataSession.convertUnitFactor(Unit.U_Meter, lDataSession.getDmuBuildUnit()),
angle: Math.PI * 0.5
};

// and make a measurement between the geometry and the given arc of circle (do not use item centers)
// the measure is the smallest distance between the 2 elements
lMeasurementRequestId = lFeatureManager.computeMeasurement(lFirstFeature, lSecondFeature, MeasurementType.MT_Item_Item);
if(lMeasurementRequestId <= 0)
{
console.log('Failed to make measurement, is the geometric instance id correct ?');
}

Export procedures

The 3djuump infinite api since the 4.1 version allows to perform asynchronous operations from the backend. 2 asynchronous operations are available :

  • 2D "complete" (high definition) screenshots : a 2D screenshot with all pixels filled with detailed high definition 3D data (low definition data is not used). Contrary to {InfiniteEngineInterface.screenshot} that is constrained with the current performance settings and the graphic card in use (we would call this a "draft" screenshot), the "complete" screenshot will load all required 3D detailed data and send the result when the process is over.
  • 3D file exports in numerous formats, along with/without BOM data export.

Export procedure are available through DataSessionInterface.export3D, DataSessionInterface.export2D and DataSessionInterface.exportSVG, monitoring of the given export procedure is available through a ExportJobInterface.

Please note that the DataSessionInterface MUST NOT be closed before retrieving the final result, since closing the given DataSessionInterface will cancel the export procedure and get rid of the exported data.

/** 
* Sample to illustrate the use of a "complete" screenshot export procedure.
*/
import {
DataSessionInterface, WorkingSetInterface, ExportJobInterface, ExportJobInterfaceSignal, InfiniteError, InfiniteEvent, InfiniteEngineInterface,
Export2DOutputFormat, Vector2, Vector4
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession: DataSessionInterface;
// the working set to export as a raster screenshot
let lWorkingSetToExport : WorkingSetInterface;
// the export job to monitor
let lExportJob : ExportJobInterface | undefined = undefined;
// the infinite engine in use, created previously
let lInfiniteEngine: InfiniteEngineInterface;

// the file to download
// In this test, we will only download one file
let lFileOffset : number = -1;

// triggers a save file from the browser
const onDownloadReady = (pUrl : string) : void => {
const downloadLink = document.createElement('a');
downloadLink.target = '_blank';
// set object URL as the anchors href
downloadLink.href = pUrl;
// append the anchor to document body
document.body.appendChild(downloadLink);
// fire a click event on the anchor
downloadLink.click();
// cleanup: remove element and revoke object URL
downloadLink.remove();
};

const startExportProcedure = () : boolean =>
{
// we will try to export as png
lExportJob = lDataSession.export2D(
[{ workingSet: lWorkingSetToExport }],
{
type: 'export2DOutputFormat',
outputs: [
{
// the name inside the screenshot procedure, as multiple viewpoint may
// be set, in this case this is useless
itemName: 'screenshot',
// the resolution of the image
resolution: new Vector2(1024, 768),
// camera parameters
viewpoint: lInfiniteEngine.getCameraManager().toJSON(),
// the background color
backgroundColor: new Vector4(1, 1, 1, 1),
// PNG to get a lossless compression
dataFormat: Export2DOutputFormat.EOF_PNG
}
],
// filename without extension
filename: 'test'
}
);
// sanity check
if(lExportJob === undefined)
{
return false;
}
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobFinished, (pEvent : InfiniteEvent) : void =>
{
// only get the URL if no error
const lError : InfiniteError | undefined = lExportJob.getLastError();
if(lError === undefined && (lFileOffset === -1))
{
lFileOffset = pEvent.attachments;
if(lExportJob.isExportSuccess(lFileOffset))
{
// trigger the URL computing
if(!lExportJob.computeDownloadURL(lFileOffset))
{
console.log('Export job failed');
}
}
}
});
// URL will be ready
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobURLReady, () : void =>
{
const lUrl : string | undefined = lExportJob.getDownloadURL(lFileOffset);
// sanity check
if(lUrl === undefined)
{
console.log('Export job failed');
return;
}
// trigger the download from the browser
onDownloadReady(lUrl);
});
return true;
};

startExportProcedure();
/** 
* Sample to illustrate the use of a 3d export procedure.
*/
import { DataSessionInterface, WorkingSetInterface, ExportJobInterface, ExportJobInterfaceSignal, InfiniteError, InfiniteEvent, Export3DOutputFormat } from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession: DataSessionInterface;
// the working set to export as 3d data
let lWorkingSetToExport : WorkingSetInterface;
// the export job to monitor
let lExportJob : ExportJobInterface | undefined = undefined;
// the file to download
// In this test, we will only download one file
let lFileOffset : number = -1;

// triggers a save file from the browser
const onDownloadReady = (pUrl : string) : void => {
const downloadLink = document.createElement('a');
downloadLink.target = '_blank';
// set object URL as the anchors href
downloadLink.href = pUrl;
// append the anchor to document body
document.body.appendChild(downloadLink);
// fire a click event on the anchor
downloadLink.click();
// cleanup: remove element and revoke object URL
downloadLink.remove();
};

const startExportProcedure = () : boolean =>
{
// will will try to export as fbx
lExportJob = lDataSession.export3D(
[{ workingSet: lWorkingSetToExport }],
{
type: 'export3DOutputFormat',
dataFormat: Export3DOutputFormat.EOF_FBX,
// filename without extension
filename: 'test'
}
);
// sanity check
if(lExportJob === undefined)
{
return false;
}
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobFinished, (pEvent : InfiniteEvent) : void =>
{
// only get the URL if no error
const lError : InfiniteError | undefined = lExportJob.getLastError();
if(lError === undefined && (lFileOffset === -1))
{
lFileOffset = pEvent.attachments;
if(lExportJob.isExportSuccess(lFileOffset))
{
// trigger the URL computing
if(!lExportJob.computeDownloadURL(lFileOffset))
{
console.log('Export job failed');
}
}
}
});
// URL will be ready
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobURLReady, () : void =>
{
const lUrl : string | undefined = lExportJob.getDownloadURL(lFileOffset);
// sanity check
if(lUrl === undefined)
{
console.log('Export job failed');
return;
}
// trigger the download from the browser
onDownloadReady(lUrl);
});
return true;
};

startExportProcedure();
/** 
* Sample to illustrate the use of a SVG export procedure.
*/
import {
DataSessionInterface, WorkingSetInterface, ExportJobInterface, ExportJobInterfaceSignal, InfiniteError, InfiniteEvent,
InfiniteEngineInterface, Vector2, ExportSVGOutputFormat
} from 'generated_files/documentation/appinfiniteapi';

// the DataSessionInterface has been created previously and is connected
let lDataSession: DataSessionInterface;
// the working set to export as a svg image
let lWorkingSetToExport : WorkingSetInterface;
// the export job to monitor
let lExportJob : ExportJobInterface | undefined = undefined;
// the infinite engine in use, created previously
let lInfiniteEngine: InfiniteEngineInterface;

// the file to download
// In this test, we will only download one file
let lFileOffset : number = -1;

// triggers a save file from the browser
const onDownloadReady = (pUrl : string) : void => {
const downloadLink = document.createElement('a');
downloadLink.target = '_blank';
// set object URL as the anchors href
downloadLink.href = pUrl;
// append the anchor to document body
document.body.appendChild(downloadLink);
// fire a click event on the anchor
downloadLink.click();
// cleanup: remove element and revoke object URL
downloadLink.remove();
};

const startExportProcedure = () : boolean =>
{
// will will try to export as svg
lExportJob = lDataSession.exportSVG(
[{ workingSet: lWorkingSetToExport }],
{
type: 'exportSVGOutputFormat',
outputs: [
{
// the name inside the screenshot procedure, as multiple viewpoint may
// be set, in this case this is useless
itemName: 'svg_screenshot',
// the page size in mm (A4 here)
pageSize: new Vector2(210, 287),
// camera parameters
viewpoint: lInfiniteEngine.getCameraManager().toJSON(),
// SVG file format
dataFormat: ExportSVGOutputFormat.EOF_SVG
}
],
// filename without extension
filename: 'test'
}
);
// sanity check
if(lExportJob === undefined)
{
return false;
}
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobFinished, (pEvent : InfiniteEvent) : void =>
{
// only get the URL if no error
const lError : InfiniteError | undefined = lExportJob.getLastError();
if(lError === undefined && (lFileOffset === -1))
{
lFileOffset = pEvent.attachments;
if(lExportJob.isExportSuccess(lFileOffset))
{
// trigger the URL computing
if(!lExportJob.computeDownloadURL(lFileOffset))
{
console.log('Export job failed');
}
}
}
});
// URL will be ready
lExportJob.addEventListener(ExportJobInterfaceSignal.ExportJobURLReady, () : void =>
{
const lUrl : string | undefined = lExportJob.getDownloadURL(lFileOffset);
// sanity check
if(lUrl === undefined)
{
console.log('Export job failed');
return;
}
// trigger the download from the browser
onDownloadReady(lUrl);
});
return true;
};

startExportProcedure();

Picking

Picking is the action to determine which object(s) is/are at specific 2D positions inside a rendering area. This feature is handled with the InfiniteEngineInterface with the use of the InfiniteEngineInterface.pickAt, InfiniteEngineInterface.pickRect and InfiniteEngineInterface.pickFromRay functions. Indeed, it is possible to get the :

  • geometries
  • points
  • lines
  • boxes
  • annotations
  • features

under an area (a point or a rectangle). The picking result is handled by the PickingAttachment interface, which stores for each picked object :

  • its id (geometric instance id/point id/line id/box id/annotation id)
  • its closest 3D position (a virtual ray is casted on the scene for each picking position, the resulting point is the closest point of the set of the intersection of these rays and the object)
  • the number of times this object is intersected (minimum one)
  • the id of the picking request (see InfiniteEngineInterface.getLastPickingRequestId).
  • the list of features (characteristic lines and circles of 3D geometries).

Each function also provides a boolean pPickOnlyClosest that, if set, will keep only the closest item of all types, discarding other types if further.

The picking procedure depends on the elements that are rendered at the time of the picking, if an element is not rendered because it is behind a closer object, or for performance reasons (memory limit reached, too many triangles in the scene), then such an object will not be present in the pick result. For developers accustomed to 3D rendering, such a picking is not a frustum picking but a rendering based picking (not software but hardware based).

The Picking coordinate system follows the given convention (x,y):

picking convention
/** 
* Sample to illustrate the handling of a mouse pick in an InfiniteEngineInterface.
*/
import {
InfiniteEngineInterface, InfiniteFactory, PickingAttachment, PickingAttachmentItem,
Vector3, VisualStates, InfiniteEngineInterfaceSignal, Rectangle, InfiniteEvent,
PrimitiveManagerInterface
} from 'generated_files/documentation/appinfiniteapi';

// the div to render to
let lView3D: HTMLElement;

// create an 3D engine
const lInfiniteEngine: InfiniteEngineInterface = InfiniteFactory.CreateInfiniteEngine();

// What to do on pick ?
let onButtonClicked : (pEvent) => void = undefined;

// What to do on pick ?
let onPicking : (pEvent : InfiniteEvent, _pCallbackData) => void = undefined;

// We pick a rectangle if big enough, else only a point
let pickAtImpl : () => void = undefined;

// bind the 3D rendering to the div
lInfiniteEngine.setView(lView3D);

// ####################################################################################################################################
// ############################################ PICKING ###############################################################################
// ####################################################################################################################################
// Used to copy the relevant portion of a MouseEvent
// position, previous buttons, current button and type
// store also if we started showing the rubber band
interface EventPos
{
x : number;
y : number;
buttons : number;
button : number;
type : string;
// is the rubber band initialized, i.e. have we begun to draw a rectangle ?
rubberInit : boolean;
}
// store the last click to know the extent of the rectangle to pick, and after pick request
// to know the properties of the pick request
// we store a copy of the MouseEvent
const lLastClickEvent: EventPos = {
x: 0,
y: 0,
buttons: 0,
button: 0,
type: '',
rubberInit: false,
};

// What to do on pick ?
onPicking = (pEvent : InfiniteEvent, _pCallbackData) : void =>
{
const lAttachment: PickingAttachment = <PickingAttachment>pEvent.attachments;
// care only about 3d geometries (no line, point, box)
const l3dAttachment: PickingAttachmentItem[] | undefined = lAttachment.geometric;

let lFirstGeometricId: number = 0;
let l3dPosition: Vector3 | undefined = undefined;
if (l3dAttachment !== undefined && l3dAttachment.length > 0)
{
lFirstGeometricId = l3dAttachment[0].instanceId;
l3dPosition = l3dAttachment[0].position;
}

// Mid button click => set Center Of Interest (COI)
if (lLastClickEvent.button === 1) {
if (!l3dPosition) return;
// set the center coi
lInfiniteEngine.getCameraManager().lookAt(l3dPosition);
return;
}

// no left button click => do nothing
if (lLastClickEvent.button !== 0)
{
return;
}
// if double click
if (lLastClickEvent.type === 'dblclick')
{
// fit to geometry
lInfiniteEngine.getCameraManager().fitGeometry(lFirstGeometricId);
return;
}
// clear selected state on pick
let lWasSelected: boolean = false;
if (lFirstGeometricId !== 0)
{
const lGeomState = lInfiniteEngine.getGeometricState(lFirstGeometricId);
lWasSelected = ((lGeomState & VisualStates.S_Selected) !== 0);
}
// if multiple selections => do not change the selected state of a single geometry
// but select all the relevant 3D data instead
if (l3dAttachment && l3dAttachment.length > 1)
{
lWasSelected = false;
}
// unselect everyone
lInfiniteEngine.updateGeometricStateForAll(VisualStates.S_Selected, ~VisualStates.S_Selected);
// and begin
if (lFirstGeometricId !== 0 && !lWasSelected && l3dAttachment)
{
// update visual state of selected items
const lIds = new Uint32Array(l3dAttachment.length);
for (let i = 0; i < l3dAttachment.length; i += 1) {
lIds[i] = l3dAttachment[i].instanceId;
}
lInfiniteEngine.updateGeometricState(lIds, VisualStates.S_Selected, VisualStates.S_Selected);
}
};

// and bind the callback on pick result
lInfiniteEngine.addEventListener(InfiniteEngineInterfaceSignal.Picked, onPicking);

// the rectangle to pick (if relevant)
const lRubberBand: Rectangle = new Rectangle();

// We pick a rectangle if big enough, else only a point
pickAtImpl = (): void =>
{
// single pick if small rectangle
if (((lRubberBand.width <= 3) && (lRubberBand.height <= 3)) || (lRubberBand.width === 0) || (lRubberBand.height === 0))
{
// pick the last point
lInfiniteEngine.pickAt(lLastClickEvent.x, lLastClickEvent.y, false);
}
else
{
// pick an area
lInfiniteEngine.pickRect(lRubberBand, false);
}
// and clear the rectangle in all the cases
lRubberBand.clear();
};

onButtonClicked = (event) : void =>
{
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;

if (lLastClickEvent.buttons === 0)
{
// make a pick
pickAtImpl();
}
};
// on left click => pick
lView3D.addEventListener('click', onButtonClicked);

const onMouseDown = (event) : void =>
{
// store the pick start
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
// no rubber
lLastClickEvent.rubberInit = false;
};
// store the position to the first pick pos
lView3D.addEventListener('mousedown', onMouseDown);

// when we release the mouse button, make a pick only on middle mouse if close to start pos
// and hide the rubber band
const onMouseUp = (event) : void =>
{
// middle click
if (lLastClickEvent.button === 1 && event.button === 1)
{
if (!lLastClickEvent.rubberInit) {
// if no rectangle => set COI
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;
pickAtImpl();
}
}
const lPrimitiveManager : PrimitiveManagerInterface = lInfiniteEngine.getPrimitiveManager();
lPrimitiveManager.getRubberBandManager().setRubberBandVisible(false);
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;
};

lView3D.addEventListener('mouseup', onMouseUp);

// we we move the mouse, display the rubber band
const onMouseMove = (event) : void =>
{
if (lLastClickEvent.rubberInit || (Math.abs(lLastClickEvent.x - event.offsetX) > 3 || Math.abs(lLastClickEvent.y - event.offsetY) > 3))
{
lRubberBand.x = Math.min(lLastClickEvent.x, event.offsetX);
lRubberBand.y = Math.min(lLastClickEvent.y, event.offsetY);
lRubberBand.width = Math.abs(lLastClickEvent.x - event.offsetX);
lRubberBand.height = Math.abs(lLastClickEvent.y - event.offsetY);
lLastClickEvent.rubberInit = true;
}
if ((lLastClickEvent.buttons === 1) && (event.buttons === 1) && lLastClickEvent.rubberInit)
{
// show/update the rubber band
const lPrimitiveManager: PrimitiveManagerInterface = lInfiniteEngine.getPrimitiveManager();
lPrimitiveManager.getRubberBandManager().setRubberBandRectangle(lRubberBand);
lPrimitiveManager.getRubberBandManager().setRubberBandVisible(true);
}
};
lView3D.addEventListener('mousemove', onMouseMove);

or with async calls :

/** 
* Sample to illustrate the handling of an asynchronous mouse pick in an InfiniteEngineInterface.
*/
import {
InfiniteEngineInterface, InfiniteFactory, PickingAttachment, PickingAttachmentItem, Vector3, VisualStates, AsyncPickingResult,
Rectangle, AsyncResultReason, PrimitiveManagerInterface,
} from 'generated_files/documentation/appinfiniteapi';

// the div to render to
let lView3D: HTMLElement;

// create an 3D engine
const lInfiniteEngine: InfiniteEngineInterface = InfiniteFactory.CreateInfiniteEngine();

// What to do on pick ?
let onButtonClicked : (pEvent) => void = undefined;

// We pick a rectangle if big enough, else only a point
let pickAtImpl : () => Promise<void> = undefined;

// bind the 3D rendering to the div
lInfiniteEngine.setView(lView3D);

// ####################################################################################################################################
// ############################################ PICKING ###############################################################################
// ####################################################################################################################################
// Used to copy the relevant portion of a MouseEvent
// position, previous buttons, current button and type
// store also if we started showing the rubber band
interface EventPos
{
x : number;
y : number;
buttons : number;
button : number;
type : string;
// is the rubber band initialized, i.e. have we begun to draw a rectangle ?
rubberInit : boolean;
}
// store the last click to know the extent of the rectangle to pick, and after pick request
// to know the properties of the pick request
// we store a copy of the MouseEvent
const lLastClickEvent: EventPos = {
x: 0,
y: 0,
buttons: 0,
button: 0,
type: '',
rubberInit: false,
};

// What to do on pick ?
const onPicking = (pAttachment: PickingAttachment) : void =>
{
// care only about 3d geometries (no line, point, box)
const l3dAttachment: PickingAttachmentItem[] | undefined = pAttachment.geometric;

let lFirstGeometricId: number = 0;
let l3dPosition: Vector3 | undefined = undefined;
if (l3dAttachment !== undefined && l3dAttachment.length > 0)
{
lFirstGeometricId = l3dAttachment[0].instanceId;
l3dPosition = l3dAttachment[0].position;
}

// Mid button click => set Center Of Interest (COI)
if (lLastClickEvent.button === 1) {
if (!l3dPosition) return;
// set the center coi
lInfiniteEngine.getCameraManager().lookAt(l3dPosition);
return;
}

// no left button click => do nothing
if (lLastClickEvent.button !== 0)
{
return;
}
// if double click
if (lLastClickEvent.type === 'dblclick')
{
// fit to geometry
lInfiniteEngine.getCameraManager().fitGeometry(lFirstGeometricId);
return;
}
// clear selected state on pick
let lWasSelected: boolean = false;
if (lFirstGeometricId !== 0)
{
const lGeomState = lInfiniteEngine.getGeometricState(lFirstGeometricId);
lWasSelected = ((lGeomState & VisualStates.S_Selected) !== 0);
}
// if multiple selections => do not change the selected state of a single geometry
// but select all the relevant 3D data instead
if (l3dAttachment && l3dAttachment.length > 1)
{
lWasSelected = false;
}
// unselect everyone
lInfiniteEngine.updateGeometricStateForAll(VisualStates.S_Selected, ~VisualStates.S_Selected);
// and begin
if (lFirstGeometricId !== 0 && !lWasSelected && l3dAttachment)
{
// update visual state of selected items
const lIds = new Uint32Array(l3dAttachment.length);
for (let i = 0; i < l3dAttachment.length; i += 1) {
lIds[i] = l3dAttachment[i].instanceId;
}
lInfiniteEngine.updateGeometricState(lIds, VisualStates.S_Selected, VisualStates.S_Selected);
}
};

// the rectangle to pick (if relevant)
const lRubberBand: Rectangle = new Rectangle();

// We pick a rectangle if big enough, else only a point
pickAtImpl = async () : Promise<void> =>
{
let lPickingResult : AsyncPickingResult;
// single pick if small rectangle
if (((lRubberBand.width <= 3) && (lRubberBand.height <= 3)) || (lRubberBand.width === 0) || (lRubberBand.height === 0))
{
// pick the last point
lPickingResult = await lInfiniteEngine.asyncPickAt(lLastClickEvent.x, lLastClickEvent.y, false);
}
else
{
// pick an area
lPickingResult = await lInfiniteEngine.asyncPickRect(lRubberBand, false);
}
// and clear the rectangle in all the cases
lRubberBand.clear();
if (lPickingResult.reason !== AsyncResultReason.ARR_Success || lPickingResult.value === undefined)
{
return;
}
onPicking(lPickingResult.value);
};

onButtonClicked = (event) : void =>
{
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;

if (lLastClickEvent.buttons === 0)
{
// make a pick
pickAtImpl();
}
};
// on left click => pick
lView3D.addEventListener('click', onButtonClicked);

const onMouseDown = (event) : void =>
{
// store the pick start
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
// no rubber
lLastClickEvent.rubberInit = false;
};
// store the position to the first pick pos
lView3D.addEventListener('mousedown', onMouseDown);

// when we release the mouse button, make a pick only on middle mouse if close to start pos
// and hide the rubber band
const onMouseUp = (event) : void =>
{
// middle click
if (lLastClickEvent.button === 1 && event.button === 1)
{
if (!lLastClickEvent.rubberInit) {
// if no rectangle => set COI
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;
pickAtImpl();
}
}
const lPrimitiveManager : PrimitiveManagerInterface = lInfiniteEngine.getPrimitiveManager();
lPrimitiveManager.getRubberBandManager().setRubberBandVisible(false);
lLastClickEvent.x = event.offsetX;
lLastClickEvent.y = event.offsetY;
lLastClickEvent.button = event.button;
lLastClickEvent.buttons = event.buttons;
lLastClickEvent.type = event.type;
lLastClickEvent.rubberInit = false;
};

lView3D.addEventListener('mouseup', onMouseUp);

// we we move the mouse, display the rubber band
const onMouseMove = (event) : void =>
{
if (lLastClickEvent.rubberInit || (Math.abs(lLastClickEvent.x - event.offsetX) > 3 || Math.abs(lLastClickEvent.y - event.offsetY) > 3))
{
lRubberBand.x = Math.min(lLastClickEvent.x, event.offsetX);
lRubberBand.y = Math.min(lLastClickEvent.y, event.offsetY);
lRubberBand.width = Math.abs(lLastClickEvent.x - event.offsetX);
lRubberBand.height = Math.abs(lLastClickEvent.y - event.offsetY);
lLastClickEvent.rubberInit = true;
}
if ((lLastClickEvent.buttons === 1) && (event.buttons === 1) && lLastClickEvent.rubberInit)
{
// show/update the rubber band
const lPrimitiveManager: PrimitiveManagerInterface = lInfiniteEngine.getPrimitiveManager();
lPrimitiveManager.getRubberBandManager().setRubberBandRectangle(lRubberBand);
lPrimitiveManager.getRubberBandManager().setRubberBandVisible(true);
}
};
lView3D.addEventListener('mousemove', onMouseMove);

Transformations

You may translate/rotate geometric models with the InfiniteEngineInterface.addMatrix, InfiniteEngineInterface.setMatrix. Functions involving the 3D positioning on the InfiniteEngineInterface works with transformed instances, whereas functions on the DataSessionInterface provides functions instances when they are untransformed.

/** 
* Sample to illustrate how to change the matrix of a set of `geometric instances`.
*/
import { DataSessionInterface, InfiniteEngineInterface, Matrix4, Unit, Vector3 } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;
// created previously
let lDataSession : DataSessionInterface;

// the geometric instance ids to translate
let lCurrentGeometries : Uint32Array;

// the resulting transformation
const lTransformMatrix : Matrix4 = new Matrix4();
// we will translate 1 meter
const lOneMeter : number = 1 * lDataSession.convertUnitFactor(Unit.U_Meter, lDataSession.getDmuBuildUnit());

// we will want to translate the given pieces of 1 meter in the up direction
const lZVector : Vector3 = new Vector3();
lInfiniteEngine.getCameraManager().getFrameReference(undefined, lZVector);
lZVector.scale(lOneMeter);

// set this as the translation
lTransformMatrix.setTranslationPart(lZVector.x, lZVector.y, lZVector.z);
// and translate the given part with this value
lInfiniteEngine.addMatrix(lCurrentGeometries, lTransformMatrix);

You may also 'explode' geometries in one or multiple directions, objects are translated depending on the distance between the center of the explosion and the center of each object.

engine explosion upon x axis
/** 
* Sample to illustrate how to set an explosion for a set of `geometric instances`.
*/
import { InfiniteEngineInterface, Matrix4, Vector3 } from 'generated_files/documentation/appinfiniteapi';

// created previously
let lInfiniteEngine : InfiniteEngineInterface;

// the geometric instance ids to translate
let lCurrentGeometries : Uint32Array;

// the direction we want the transform to apply
// created previously
let lExplodedDirection : Vector3;

// the center of the explosion
// created previously
let lExplosionCenter : Vector3;

// other directions
const lOtherDirections : Array<Vector3> = [new Vector3(1, 0, 0), new Vector3()];

// normalize the vector, just in case, but it may be useless
lExplodedDirection.normalize();

// create a new unit frame with lExplodedDirection
lExplodedDirection.crossVector(lOtherDirections[0], lOtherDirections[1]);
if(lOtherDirections[1].lengthSquare() === 0)
{
// lExplodedDirection is colinear to 1, 0, 0 ?
lOtherDirections[0].set(0, 1, 0);
lExplodedDirection.crossVector(lOtherDirections[0], lOtherDirections[1]);
}
lOtherDirections[1].normalize();
lOtherDirections[1].crossVector(lExplodedDirection, lOtherDirections[0]);
// this should not be necessary
lOtherDirections[0].normalize();

// now we have a direct orthonormal frame with lExplodedDirection, lOtherDirections[0], lOtherDirections[1]

// we will explode distances upon lExplodedDirection multiplying distance by 2
const lScaleFactor : number = 2;

const lScaleMatrix : Matrix4 = new Matrix4();
lScaleMatrix.makeIdentity();
lScaleMatrix.array[0] = lScaleFactor;

// the rotation matrix from the current frame to the new frame [lExplodedDirection, lOtherDirections[0], lOtherDirections[1]]
const lRotationMatrix : Matrix4 = new Matrix4();
lRotationMatrix.set(lExplodedDirection.x, lExplodedDirection.y, lExplodedDirection.z, 0, lOtherDirections[0].x, lOtherDirections[0].y, lOtherDirections[0].z, 0, lOtherDirections[1].x, lOtherDirections[1].y, lOtherDirections[1].z, 0, 0, 0, 0, 1);
// the inverse rotation matrix => transpose
const lInverseRotationMatrix : Matrix4 = new Matrix4();
lInverseRotationMatrix.set(lExplodedDirection.x, lOtherDirections[0].x, lOtherDirections[1].x, 0, lExplodedDirection.y, lOtherDirections[0].y, lOtherDirections[1].y, lExplodedDirection.z, lOtherDirections[0].z, 0, lOtherDirections[1].z, 0, 0, 0, 0, 1);

lScaleMatrix.multiplyMatrixLeft(lRotationMatrix);
lScaleMatrix.multiplyMatrixRight(lInverseRotationMatrix);
// lScaleMatrix now holds the explosion in the given specific direction.

// and explode the geometries
lInfiniteEngine.addExplodedInstances(lCurrentGeometries, lExplosionCenter, lScaleMatrix);

Use cases

This section points the developer to the relevant sections of the documentation.

You want to :

What's new

Versions log

4.1.20

  • No change.

4.1.19 03/12/2025

4.1.18 06/11/2025

4.1.17 22/10/2025

4.1.16 20/10/2025

  • No change.

4.1.15

  • Updated webxr requirements to avoid warning messages, and fixed some texture not being correctly updated when starting.
  • Fixed some WebGL errors that may happen on ray picks.
  • Added InfiniteEngineInterface.getXRSession to access the current XR session (if any, in case of WebXR view).
  • Fixed a rendering bug that made geometries to flick when the given instances had reflected instances and were transformed.
  • Changed the XR camera controller mode, sticks have been swapped, and the XR camera mode has now its own sensibility.
  • Updated CameraManagerInterface documentation to reflect the changes in the XR camera controller mode.

4.1.14 24/09/2025

  • No change.

4.1.13 22/09/2025

  • No change.

4.1.12 18/09/2025

  • No change.

4.1.11 16/09/2025

4.1.10 10/09/2025

  • No change.

4.1.9 12/08/2025

  • No change.

4.1.8 06/08/2025

  • No change.

4.1.7 31/07/2025

  • Updated api to account for features migrated from older versions.

4.1.6 28/07/2025

4.1.5 22/07/2025

  • No change, some fixes that should have been included in the 4.1.4 are now actually included.

4.1.4 21/07/2025

4.1.3 26/06/2025

4.1.2 06/05/2025

4.1.1 15/04/2025

  • Updated samples to use webpack and npm, adding a more precise example on the correct usage of the infinite api.
  • Updated web-worker loading mechanism to allow using web-workers even in CORS configuration. InfiniteApiControllerInterface.setWebWorkerPath and infiniteapi-webworker-loader.js file have been included to do so.

4.1.0 10/04/2025

  • First release.

New functionalities

3D lines and circles, patch borders, known as features are now retrieved from the 3D data, and can now be used / drawn. Specific features can be created from scratch, and displayed.

A measurement module as been created to allow performing distance between features. For example, the minimal distance between 2 geometries can now be performed.

Export procedures (2D "complete" screenshot and 3D files) are now available.

"Explosions" are now available as a new type of transformation.

The 3djuump infinite api now uses web-workers, they may allow better frame rates by discharging the main browser thread from decoding 3d data.

Increased performance on a lot of browsers, that may lead to taking more time to get the correct 3D set to display, but this increases the framerate globally.

Annotations now use roughly less 25% less VRAM than before.

New internal behavior from browser were introduced in 2025 when the current tab is hidden, inactive / idle. A specific implementation has been added to avoid premature data session closing due to this new mechanism.

The PrimitiveManagerInterface can now be dumped with PrimitiveManagerInterface.toJSON and PrimitiveManagerInterface.fromJSON.

Some WorkingSetInterface can now be excluded from streaming (namely DataSessionInterface.toJSON) with WorkingSetInterface.setExcludedFromJSONStreaming.

When a DMU is loaded, the camera perspective is automatically reset to the parameters set by the project maintainer. This behavior can now be disabled with CameraManagerInterface.setPerspectiveChangedOnDMULoaded.

Navigation cube names can now be customized with CameraManagerInterface.setNavigationCubeFacesName.

With the camera in orbit mode, the Center Of Interest (COI) can now be modified while zooming by hitting the SHIFT key, allowing to go through obstacles.

Migrating from 4.0 version

This section lists the main changes to allow the migration to the current version.

Please keep in mind that each version of the api comes with the retro-compatibility code that allows previous code to still work on the updated 3dJuump infrastructure.

You will be loading your api from
https://my_directory/directory/api/getwebapi?version=4.0 if you are using api version 4.0, or https://my_directory/directory/api/getwebapi?version=3.3 if you are using api version 3.3, or https://my_directory/directory/api/getwebapi?version=3.2 if you are using api version 3.2, or https://my_directory/directory/api/getwebapi?version=3.1 if you are using api version 3.1.

Remember that references to 3djuump Infinite api calls should be set as external calls (in order not to bundle the 3djuump api in your final javascript api file), and reference those calls to the use of the infiniteapi (Please see Getting started).

Old code should still work but without any possibility to use the new developments.

It is highly recommended to update your code base to benefit from the new features.

Migrating to the new web api should take less than a full working day.

Annotations

New function has been added to wait for some annotation requests to complete (AnnotationRendererInterface.asyncWaitForAnnotationRequests).

Annotation parsing can now be cancelled by calling AnnotationRendererInterface.removeAnnotationViews with request ids.

The api was returning -1 when an error occurred upon calling AnnotationRendererInterface.addAnnotationView. AnnotationRendererInterface.addAnnotationView now returns 0 in case of error, and annotation requests ids are now strictly negative numbers, thus allowing to know when calling AnnotationRendererInterface.removeAnnotationViews if the caller is expecting to cancel a request or removing already parsed annotation views.

Rendering

InfiniteEngineInterface.projectToScreen has been changed. The previous result was relative to the internal canvas that is used by the library. The result is now relative to the view that was set.

A new event InfiniteApiControllerInterfaceSignal.AboutToRun has been added, sent before all objects are updated (a while before InfiniteApiControllerInterfaceSignal.Tick is triggered).

Camera

The smooth zooming in orbit mode (used with middle mouse + long right click) may be also performed with middle mouse + CTRL key. If SHIFT is pressed, the Center Of Interest (COI) is also modified. This allows to go through obstacles.

Known bugs

The filtering of metadata in search requests is not implemented yet, full documents are sent. The 2D export procedure do not draw annotations, features and 3D primitives at the moment.

License

This program belongs to AKKODIS INGENIERIE PRODUIT SAS.
It is considered a trade secret, and is not to be divulged or used
by parties who have not received written authorization from the owner.


This package includes the opentype.js library, licensed under the MIT license terms. This package includes the bowser library, licensed under the MIT license terms. This package includes the xmldom library, licensed under the MIT license terms.

The MIT License (MIT)
Copyright (c) 2017 Frederik De Bleser
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


This package includes the luxon library, licensed under the JS Foundation license.

Copyright 2019 JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.