SDKPackagesAssetAsset Renderer

Asset Renderer SDK

The @sdk/asset-renderer SDK provides a loose framework to render HxDR assets in a web application.

Installation

To install this package, you need to have access to the private @sdk npm scope.

To install the library, run the following command in your project directory:

npm install @sdk/asset-renderer

Asset Renderer Plugin System

The Asset Renderer Plugin System is a modular and extensible framework designed to enhance a centralized 3D representation of scan data, such as point clouds and meshes. It allows developers to add features (e.g., measurements, navigation controls, or dollhouse modes) in a structured and maintainable way. The system promotes code organization, separation of concerns, and team collaboration by providing a unified API for plugin development.

⚠️

The asset renderer is a specific implementation of the Renderer Plugin System. All best practices described in that document therefore also apply to this system.

Setting Up the Asset Renderer Plugin System

1. Create a AssetRendererPluginManager

The AssetRendererPluginManager is the central hub for managing plugins. It requires a AssetRendererPluginContext, which provides shared resources like the 3D canvas, asset data, and configuration.

const rendererPluginContext = AssetRendererPluginContext.create({
  asset, // The asset (type `AssetCore.AssetRenderableProspect`) as loaded from the `@sdk/asset-core` library
  authTokenResolver, // A function to resolve authentication tokens
  canvas: htmlCanvasRef, // The HTML canvas element for rendering
  config, // A `Config.MandatoryConfig` from the `@sdk/config` library
  panoramaFloorTexture, // Optional texture for panoramic floors
});
 
const rendererPluginManager = new AssetRendererPluginManager(rendererPluginContext);

2. Register Plugins

Plugins can be registered with the AssetRendererPluginManager using the registerPlugin method.

rendererPluginManager.registerPlugin(new MyPlugin());

3. Initialize Plugins and Layers

After registering plugins, call the initialize method to initialize the plugins. This will also trigger the internal loading of the associated layers of the asset.

rendererPluginManager.initialize();

3a. Retrieve Plugins

Plugins can be retrieved using either getPlugin or getPluginSafe:

  • getPlugin: Retrieves a plugin by its constructor. Throws an error (TypeError) if the plugin is not registered. This is useful when your code expects the plugin to be registered.

    const myPlugin = rendererPluginManager.getPlugin(MyPlugin);
  • getPluginSafe: Retrieves a plugin by its constructor. Returns undefined if the plugin is not registered.

    const myPlugin = rendererPluginManager.getPluginSafe(MyPlugin);
    if (myPlugin) {
      // Use the plugin
    }

4. Teardown Plugins

When the renderer is no longer needed, call the teardown method to clean up resources and remove event listeners.

rendererPluginManager.teardown();

Writing a Plugin

Creating a Plugin Class

Plugins must implement the AssetRendererPlugin interface. For common functionality like event management you can make use of the renderer plugin mixin system.

Example: Basic Plugin

type CullingPluginEvents = {
  onChangeCullingAvailability: { isAvailable: boolean };
};
 
export class CullingPlugin extends Mix(Evented<CullingPluginEvents>()) implements AssetRendererPlugin {
  private readonly layerManager: LayerManager;
 
  constructor(context: AssetRendererPluginContext) {
    super();
    // Save instance of LayerManager for later use in enableCulling and disableCulling methods
    this.layerManager = context.getLayerManager();
  }
 
  // We hook into the ready event to notify listeners about culling availability.
  // This feature is only available if a mesh layer exists.
  public hookReadyToDisplay(): void {
    this.dispatchEvent('onChangeCullingAvailability', { isAvailable: this.isCullingAvailable() });
  }
 
  public enableCulling(): void {
    const meshLayer = this.layerManager.getMeshLayer();
    if (Option.isSome(meshLayer)) {
      Option.unwrap(meshLayer).getInternalLayer().meshStyle.facetCulling = FacetCullingType.BACKFACE_CULLING;
    }
  }
 
  public disableCulling(): void {
    const meshLayer = this.layerManager.getMeshLayer();
    if (Option.isSome(meshLayer)) {
      Option.unwrap(meshLayer).getInternalLayer().meshStyle.facetCulling = FacetCullingType.NO_CULLING;
    }
  }
 
  public isCullingAvailable(): boolean {
    return Option.isSome(this.layerManager.getMeshLayer());
  }
}

In this example, the CullingPlugin class enables backface culling on the mesh layer when the enableCulling method is called. It also emits an event when the plugin is ready to notify listeners about the availability of culling.

Plugin Lifecycle Hooks

The lifecycle of the core renderer plugin system is extended in the asset renderer. In an asset renderer plugin, you can make use of the following lifecycle hooks:

Initialization Phase (Called in Order)

  1. hookRegister: Called when the plugin is registered. Determines whether the plugin should be enabled.
  2. hookInitialize: Called when the renderer is manually initialized. Layer information is not yet available.
  3. hookStartup: Called after initialization and layer creation. Scene dimensions are not yet available.
  4. hookSceneDimensionsAvailable: Called after scene dimensions are calculated.
  5. hookReadyToDisplay: Called when enough data is loaded to display the scene.

Event-Based Hooks (Called as Needed)

  • hookLayerCreated: Called when a layer is created.
  • hookAllLayersCreated: Called when all layers are created.
  • hookLoadProgress: Called during incremental layer data loading.
  • hookLoadError: Called when an error occurs during layer data loading.
  • hookLayerShow: Called when a layer is shown.
  • hookLayerHide: Called when a layer is hidden.

Teardown

  • hookTeardown: Called when the plugin is torn down. Use this to clean up resources.

Plugin Context

The AssetRendererPluginContext provides access to shared resources like the 3D canvas, asset data, and configuration. It is passed as a parameter to every plugin hook. It contains:

  • RenderContext: The main rendering context providing access to the 3D canvas, configuration, authorization tokens, and the camera.
  • AssetContext: The asset data loaded from the @sdk/asset-core library. Including information about the asset bounds.
  • LayerManager: A manager for layers, including mesh, point cloud, and panoramic layers.
  • LayerGroupsContext: A context for managing layer groups, like the main rendering group and the overlay/tool group.
  • ControllerContext: A context for managing controllers, where plugins can register custom controllers.
class WASDControllerPlugin implements RendererPluginInterface {
  constructor(context: AssetRendererPluginContext) {
    this.wasdController = new WASDController(context.getControllerContext().getEventContext());
  }
 
  hookInitialize(context: AssetRendererPluginContext): void {
    context.getControllerContext().setNavigationController(this.wasdController);
  }
 
  hookTeardown(context: AssetRendererPluginContext): void {
    context.getControllerContext().setNavigationController(null);
  }
}

In this imaginary example, the WASDControllerPlugin registers a custom WASD controller with the ControllerContext during initialization and removes it during teardown, showing the usage of the RenderPluginContext passed through the hooks.

Extensions

Our plugin system’s best practices dictates that plugins should be isolated from eachother and not communicate directly. In order to wire plugins together in a clear and structured way, you can make use of the extension system.

The extension system sits on top of the plugin system and provides a way to bundle multiple plugins and their configuration together. This is useful for creating reusable feature sets that can be easily added to an application. This system consists of three different parts:

  • RenderExtension: An interface that defines the structure of an extension. It has two main methods:
    • registerPlugin: Called when the extension is registered. This is where the extension can register its plugins with the AssetRendererPluginManager. As extensions don’t necessarily depend on a plugin, this method can be empty.
    • setup: Called after all extensions have been registered. This is where the extension can perform any setup that requires access to other registered plugins, like wiring plugins together.
  • RenderExtensionFactory: An interface that defines a factory for creating extensions. It has a single method:
    • create: Called to create an instance of the extension. It receives the AssetRendererPluginManager as a parameter, which can be used to access other registered plugins during setup. Factories define the configuration parameters for an extension and live therefore more closely to the composition root, like an application.
  • RenderExtensionBuilder: A class that manages the registration and setup of extensions.

While plugins and extensions are generic enough to live in the package layer, the factories can live both in the package layer or in the application layer, depending on if a factory needs application-specific configuration.

In this diagram there are three option on how to get application specific configuration into an extension:

  1. We don’t need application specific configuration. In this case, the application can use MyRenderExtensionFactory directly.
  2. We need some application specific configuration, but other configuration is generic, so we can create a factory in the application that extends the generic factory (option A in the diagram).
  3. We need a lot of application specific configuration, so we create a completely custom factory in the application (option B in the diagram).

Benefits of the Extension System

  1. Modularity: Extensions bundle related plugins and their configuration together, making it easy to add or remove features.
  2. Reusability: Extensions can be reused across different applications or projects, promoting code reuse.
  3. Simplified Setup: The extension system simplifies the setup process by providing a structured way to register and configure multiple plugins.
  4. Decoupling: Extensions can be developed independently of the core rendering logic, reducing dependencies
  5. Configuration Management: Extensions can encapsulate configuration logic, making it easier to manage settings for related plugins.

Examples of Extensions

Example: Registering a Simple Plugin through an Extension

If we just want to register a simple plugin that doesn’t need to interact with other plugins, we can use a helper extension that has no setup logic: SimpleRenderExtensionFactory. The SimpleRenderExtensionFactory takes a plugins class as a parameter and registers it with the AssetRendererPluginManager.

import { SimpleRenderExtensionFactory } from '@sdk/asset-renderer';
 
rendererExtensionBuilder
  .registerFactory(new SimpleRenderExtensionFactory(MySimplePlugin))
  // ... other extensions
  .build();

Example: Creating a Custom Extension with Setup Logic

If we want to create a custom extension that wires multiple plugins together, we can create a custom extension class that implements the RenderExtension interface. In this example, we create a MyCustomExtension that registers two plugins and wires them together in the setup method.

class MyCustomExtension implements RenderExtension {
  private readonly myCustomPlugin: MyCustomPlugin;
 
  constructor(myCustomPlugin: MyCustomPlugin) {
    this.myCustomPlugin = myCustomPlugin;
  }
 
  registerPlugin(pluginManager: AssetRendererPluginManager): void {
    pluginManager.registerPlugin(this.myCustomPlugin);
  }
 
  setup(): void {
    const dependencyPlugin = pluginManager.getPlugin(DependencyPlugin);
 
    // Always ensure that the extensions plugin is avaialable at setup time.
    if (!rendererPluginManager.getPluginSafe(MyCustomPlugin)) {
      return;
    }
 
    // Wire up the plugins
    this.myCustomPlugin.on('event', () => {
      dependencyPlugin.doSomething();
    });
  }
}

After creating the custom extension, we can create a factory for instantiating it with the required options, like it’s plugin and any configuration parameters.

import { MyCustomExtension } from './my-custom-extension';
import { MyCustomPlugin } from './my-custom-plugin';
 
type AdditionalPluginOptions = {
  // Add any additional configuration options here
};
 
class MyCustomExtensionFactory implements RenderExtensionFactory {
  constructor(private readonly options: AdditionalPluginOptions) {}
 
  create(pluginManager: AssetRendererPluginManager): RenderExtension {
    const myCustomPlugin = new MyCustomPlugin(this.options);
    return new MyCustomExtension(myCustomPlugin);
  }
}

Finally, we can register the custom extension factory with the RenderExtensionBuilder.

rendererExtensionBuilder
  .registerFactory(
    new MyCustomExtensionFactory({
      /* options */
    })
  )
  // ... other extensions
  .build();