View Decorations

A View Decoration shows application-generated graphics in a ScreenViewport in addition to the persistent (i.e. scene) geometry displayed by the Viewport itself. In contrast to the graphics from the persistent geometry (e.g. the Models), View Decorations must be re-evaluated every time a frame is rendered. In this sense, they decorate the frame with graphics that are only valid for a single frame.

View Decorators

The process of creating View Decorations starts by adding an object that implements the Decorator interface to the ViewManager via the ViewManager.addDecorator method. The most important part of the Decorate interface is the Decorator.decorate method, which is called every time iTwin.js renders a frame for any ScreenViewport. The argument to the decorate method is a DecorateContext that supplies information about the ScreenViewport being rendered, as well as methods to create and save decoration graphics. The DecorateContext.viewport member holds the target viewport. If you wish to decorate only a single viewport, you must test this member against your intended viewport.

The job of the decorate method is to supply the graphics (the Decorations) for a single frame of a single ScreenViewport.

A Decorator remains active until you call ViewManager.dropDecorator (Note: ViewManager.addDecorator returns a method that calls this for you if you wish to use it.)

A InteractiveTool can also show decorations and does not need to call the ViewManager.addDecorator method to add itself. InteractiveTool.decorate is called for the active tool to add its decorations, InteractiveTool.decorate is not called when the tool is paused by another tool such as a ViewTool. To show decorations while paused, a tool can implement InteractiveTool.decorateSuspended.

To learn how to optimize when your decorations are invalidated by using cached decorations, see the section on cached decorations.

Categories of View Decorations

Sometimes decorations are meant to intersperse with the scene geometry, and sometimes they are meant to display atop of it. For this reason, there are 3 broad categories of View Decorations:

  • View Graphic Decorations - are drawn using iTwin.js render primitives into the WebGL context.
  • Canvas Decoration - are drawn onto the 2d canvas using CanvasRenderingContext2D. Canvas decorations are always on top of View Graphic Decorations
  • HTML Decorations - are HTMLElements that are added to the DOM. HTML decorations are always on top of Canvas Decorations.

Note that a single Decorator can create multiple Decorations, from any or all of the categories above.

View Graphic Decorations

View Graphic Decorations are drawn using the iTwin.js rendering system through WebGL. There are 5 types of View Graphic Decorations, defined by the GraphicType enum.

Note that GraphicType.ViewOverlay performs the same function as Canvas Decorators and are generally less flexible and less efficient. Prefer Canvas Decorations instead.

You typically create View Graphic Decorations by calling DecorateContext.createGraphicBuilder on the context supplied to decorate, supplying the appropriate GraphicType. You then add one or more graphics to the GraphicBuilder using its methods. Finally, you add the completed graphics to the frame by calling DecorateContext.addDecorationFromBuilder. Another option is to use readGltfGraphics to produce the graphics from a glTF asset and supply them to DecorateContext.addDecoration.

GraphicBuilder decorations

The following example illustrates creating a view graphic decoration to show the IModel.projectExtents in spatial views using a GraphicBuilder:

/** Add a world decoration to display 3d graphics showing the project extents interspersed with the scene graphics. */
public decorate(context: DecorateContext): void {
  // Check view type, project extents is only applicable to show in spatial views.
  const vp = context.viewport;
  if (!vp.view.isSpatialView())
    return;

  const builder = context.createGraphicBuilder(GraphicType.WorldDecoration, undefined);
  // Set edge color to white or black depending on current view background color and set line weight to 2.
  builder.setSymbology(vp.getContrastToBackgroundColor(), ColorDef.black, 2);
  // Add range box edge geometry to builder.
  builder.addRangeBox(vp.iModel.projectExtents);
  context.addDecorationFromBuilder(builder);
}

glTF decorations

The following example illustrates creating a view graphic decoration from a glTF asset using readGltfGraphics:


/** A view decoration that draws a graphic created from a glTF asset. */
class GltfDecoration implements Decorator {
  /** A graphic created from a glTF asset. */
  private readonly _graphic: RenderGraphic;
  /** The tooltip to be displayed when the graphic is moused-over. */
  private readonly _tooltip: string;
  /** The Id of the graphic used for picking. */
  private readonly _pickableId: Id64String;

  public constructor(graphic: RenderGraphic, tooltip: string, pickableId: Id64String) {
    this._graphic = graphic;
    this._tooltip = tooltip;
    this._pickableId = pickableId;
  }

  /** Tell the display system not to recreate our graphics every time the mouse cursor moves. */
  public readonly useCachedDecorations = true;

  /** Draw our graphics into the viewport. */
  public decorate(context: DecorateContext): void {
    // Our graphics are defined in spatial coordinates so should only be drawn in a spatial view
    if (context.viewport.view.isSpatialView()) {
      // Produce a "scene" graphic so that it will be affected by lighting and other aspects of the viewport's display style.
      context.addDecoration(GraphicType.Scene, this._graphic);
    }
  }

  /** Return true if the specified Id matches our graphic's pickable Id. */
  public testDecorationHit(id: string): boolean {
    return id === this._pickableId;
  }

  /** Provide a tooltip when our graphic is moused-over. */
  public async getDecorationToolTip(): Promise<string> {
    return this._tooltip;
  }
}

/** Open a file picker to allow the user to select a .gltf or .glb file describing a glTF asset, convert the asset into a RenderGraphic,
 * then install a Decorator to display the graphic in the center of the project extents.
 * @param iModel The iModel with which the graphic will be associated. Any viewports viewing a spatial view of this iModel will be decorated with the glTF asset.
 * @returns true if the graphic was successfully created.
 */
export async function displayGltfAsset(iModel: IModelConnection): Promise<boolean> {
  // Allow the user to select the glTF asset.
  try {
    // We need to cast `window` to `any` because the TypeScript type definition doesn't expose the `showOpenFilePicker` function.
    const [handle] = await (window as any).showOpenFilePicker({
      types: [
        {
          description: "glTF",
          accept: { "model/*": [".gltf", ".glb"] },
        },
      ],
    });

    // Read the file's contents into memory.
    const file = await handle.getFile();
    const buffer = await file.arrayBuffer();

    // Allocate a new transient Id to identify the graphic so it can be picked.
    const id = iModel.transientIds.getNext();

    // Convert the glTF into a RenderGraphic.
    let graphic = await readGltfGraphics({
      gltf: new Uint8Array(buffer),
      iModel,
      pickableOptions: { id },
    });

    if (!graphic)
      return false;

    // Transform the graphic to the center of the project extents.
    const transform = Transform.createTranslation(iModel.projectExtents.center);
    const branch = new GraphicBranch();
    branch.add(graphic);
    graphic = IModelApp.renderSystem.createGraphicBranch(branch, transform);

    // Take ownership of the graphic so that it is not disposed of until we're finished with it.
    // By doing so we take responsibility for disposing of it ourselves.
    const owner = IModelApp.renderSystem.createGraphicOwner(graphic);

    // Install the decorator, using the file name as the tooltip.
    const decorator = new GltfDecoration(owner, file.name, id);
    IModelApp.viewManager.addDecorator(decorator);

    // When the iModel is closed, dispose of the graphic and uninstall the decorator.
    iModel.onClose.addOnce(() => {
      owner.disposeGraphic();
      IModelApp.viewManager.dropDecorator(decorator);
    });

    return true;
  } catch {
    return false;
  }
}

Pickable View Graphic Decorations

View Graphic Decorations are drawn into or atop the scene. To make your View Graphic Decorations pickable (i.e. allow the user to click on them to perform an action, or to give feedback when the cursor hovers over them), you must:

The following example illustrates creating a pickable view graphic decoration in order to supply a tooltip message when under the cursor:

protected _decoId?: string;

/** Add a pickable decoration that will display interspersed with the scene graphics. */
public decorate(context: DecorateContext): void {
  const vp = context.viewport;
  if (!vp.view.isSpatialView())
    return;

  // Get next available Id to represent our decoration for it's life span.
  if (undefined === this._decoId)
    this._decoId = vp.iModel.transientIds.getNext();

  const builder = context.createGraphicBuilder(GraphicType.WorldDecoration, undefined, this._decoId);
  builder.setSymbology(vp.getContrastToBackgroundColor(), ColorDef.black, 2);
  builder.addRangeBox(vp.iModel.projectExtents);
  context.addDecorationFromBuilder(builder);
}

/** Return true if supplied Id represents a pickable decoration created by this decorator. */
public testDecorationHit(id: string): boolean { return id === this._decoId; }

/** Return localized tooltip message for the decoration identified by HitDetail.sourceId. */
public async getDecorationToolTip(_hit: HitDetail): Promise<HTMLElement | string> { return "Project Extents"; }

If you have many decorations to draw with different pickable Ids, it can be more efficient to produce them using a single GraphicBuilder than producing one graphic per pickable Id. This can be achieved by calling GraphicBuilder.activatePickableId or to specify the pickable Id to associate with subsequently-added geometry.

Canvas Decorations

A CanvasDecoration is drawn atop the scene using CanvasRenderingContext2D. To add a CanvasDecoration, call DecorateContext.addCanvasDecoration from your Decorator.decorate method.

CanvasDecorators must implement CanvasDecoration.drawDecoration to supply visible graphics, by calling methods on CanvasRenderingContext2D.

CanvasDecorators may optionally include the member CanvasDecoration.position, that becomes the 0,0 point for your CanvasRenderingContext2D calls.

The following example illustrates creating a canvas decoration to show a plus symbol at the center of the view:

/** Add a canvas decoration using CanvasRenderingContext2D to show a plus symbol. */
public decorate(context: DecorateContext): void {
  const vp = context.viewport;
  const size = Math.floor(vp.pixelsPerInch * 0.25) + 0.5;
  const sizeOutline = size + 1;
  const position = context.viewport.npcToView(NpcCenter);
  position.x = Math.floor(position.x) + 0.5;
  position.y = Math.floor(position.y) + 0.5;
  const drawDecoration = (ctx: CanvasRenderingContext2D) => {
    // Show black outline (with shadow) around white line for good visibility regardless of view background color.
    ctx.beginPath();
    ctx.strokeStyle = "rgba(0,0,0,.5)";
    ctx.lineWidth = 3;
    ctx.moveTo(-sizeOutline, 0);
    ctx.lineTo(sizeOutline, 0);
    ctx.moveTo(0, -sizeOutline);
    ctx.lineTo(0, sizeOutline);
    ctx.stroke();

    ctx.beginPath();
    ctx.strokeStyle = "white";
    ctx.lineWidth = 1;
    ctx.shadowColor = "black";
    ctx.shadowBlur = 5;
    ctx.moveTo(-size, 0);
    ctx.lineTo(size, 0);
    ctx.moveTo(0, -size);
    ctx.lineTo(0, size);
    ctx.stroke();
  };
  context.addCanvasDecoration({ position, drawDecoration });
}

Markers are a type of Canvas Decoration

Pickable Canvas Decorations

To make your CanvasDecorations pickable, implement CanvasDecoration.pick and return true if the supplied point lies within your decoration's region.

If you return true from your CanvasDecoration.pick method, you can implement:

HTML Decorations

HTML Decorations are simply HTMLElements that you add to the DOM on top of your views. In your Decorator.decorate method, use DecorateContext.addHtmlDecoration to add HTML Decorations.

HTML Decorators are appended to an HTMLDivElement called "overlay-decorators" that is created by ScreenViewport.create. All children of that Div are removed every frame, so you must re-add your HTML Decorator each time your Decorator.decorate method is called.

The "overlay-decorators" Div is stacked on top of the canvas, but behind the "overlay-tooltip" Div (where tooltips are displayed.)

Decoration Precedence

The order of precedence for Decorations is:

  1. GraphicType.ViewBackground decorations are drawn behind the scene
  2. GraphicType.Scene and GraphicType.WorldDecoration decorations are drawn in the scene
  3. GraphicType.WorldOverlay and GraphicType.ViewOverlay decorations are drawn on top of the scene
  4. Canvas Decorations are drawn on top of all View Graphic decorations
  5. HTML Decorations are drawn on top of all Canvas decorations
  6. The ToolTip is on top of all HTML decorations

Within a decoration type, the last decoration drawn is on top of earlier decorations.

Cached Decorations

As described in the section about view decorators, a decorator object's decorate method is invoked to create new Decorations whenever a viewport's decorations are invalidated. Decorations are invalidated quite frequently - for example, every time the view frustum or scene changes, and even on every mouse motion. Most decorators' decorations only actually need to change when the scene changes. Having to regenerate them every time the mouse moves is quite wasteful and - for all but the most trivial decorations - can negatively impact framerate. Here is an example of a decorator that draws some complicated shape in a specified color:

class FancyDecorator {
  private _color: ColorDef; // the color of our shape decoration

  public set color(color: ColorDef): void {
    this._color = color;

    // Make sure our decorate method is called so we draw using the new color.
    // This also invalidates every other decorator's decorations!
    IModelApp.viewManager.invalidateDecorationsAllViews();
  }

  public decorate(context: DecorateContext): void {
    // ...draw a fancy shape using this._color
    // This gets called every single time the mouse moves,
    // and any other time the viewport's decorations become invalidated!
  }
}

We can avoid unnecessarily recreating decorations by defining the useCachedDecorations property on a decorator object. If this is true, then whenever the viewport's decorations are invalidated, the viewport will first check to see if it already has cached decorations for this decorator. If so, it will simply reuse them; if not, it will invoke decorate and cache the result. When the scene changes, our cached decorations will automatically be discarded. Here is the decorator from above, updated to use cached decorations:

class FancyDecorator {
  private _color: ColorDef; // the color of our shape decoration

  // Tell the viewport to cache our decorations.
  // We'll tell it when to regenerate them.
  public readonly useCachedDecorations = true;

  public set color(color: ColorDef): void {
    this._color = color;

    // Invalidate *only* this decorator's decorations.
    IModelApp.viewManager.invalidateCachedDecorationsAllViews(this);
  }

  public decorate(context: DecorateContext): void {
    // ...draw a fancy shape using this._color
    // This *only* gets called if the scene changed,
    // or if explicitly asked for our decorations to be regenerated.
  }
}

For a decorator defining the useCachedDecorations property as true, the functions ViewManager.invalidateCachedDecorationsAllViews and ScreenViewport.invalidateCachedDecorations give the decorator much tighter control over when its decorations are regenerated. This can potentially result in significantly improved performance.

Last Updated: 14 November, 2024