4.0.0 Change Notes

Table of contents:

Breaking Changes

Updated minimum requirements

A new major release of iTwin.js affords us the opportunity to update our requirements to continue to provide modern, secure, and rich libraries. Please visit our Supported Platforms documentation for a full breakdown.


Node 12 reached end-of-life in 2020, and Node 14 as well as Node 16 will do so shortly. iTwin.js 4.0 requires a minimum of Node 18.12.0, though we recommend using the latest long-term-support version.


Web browsers display 3d graphics using an API called WebGL, which comes in 2 versions: WebGL 1, released 11 years ago; and WebGL 2, released 6 years ago. WebGL 2 provides many more capabilities than WebGL 1. Because some browsers (chiefly Safari) did not provide support for WebGL 2, iTwin.js has maintained support for both versions, which imposed some limitations on the features and efficiency of its rendering system.

Over a year ago, support for WebGL 2 finally became available in all major browsers. iTwin.js now requires WebGL 2 - WebGL 1 is no longer supported. This change will have no effect on most users, other than to improve their graphics performance. However, users of iOS will need to make sure they have upgraded to iOS 15 or newer to take advantage of WebGL 2 (along with the many other benefits of keeping their operating system up to date).

IModelApp.queryRenderCompatibility will now produce WebGLRenderCompatibilityStatus.CannotCreateContext for a client that does not support WebGL 2.


Electron versions from 14 to 17 reached their end-of-life last year, and for this reason, support for these versions were dropped. To be able to drop Node 16, Electron 22 was also dropped. iTwin.js now supports Electron 23 and Electron 24.

Default RPC Registration

Previously, @itwin/core-electron and @itwin/core-mobile automatically registered the following RPCs on your behalf:

  • IModelReadRpcInterface
  • IModelTileRpcInterface
  • SnapshotIModelRpcInterface
  • PresentationRpcInterface

To be more aligned with our approach on Web and to prevent unnecessary registrations and coupling of dependencies, we are now requiring the consumer to register all RPCs they need on their end. Please refer to the documentation for ElectronApp.startup and MobileHost.startup.

Breaking out of lockstep

To move more quickly and release independently, the following packages have broken out of lockstep with iTwin.js Core and have moved outside of the itwinjs-core repository.


The source code for the following packages was moved to the new AppUi repository.

  • @itwin/appui-react
  • @itwin/appui-layout-react
  • @itwin/components-react
  • @itwin/core-react
  • @itwin/imodel-components-react


The source code for the following packages was moved to the new Presentation repository.

  • @itwin/presentation-components
  • @itwin/presentation-opentelemetry
  • @itwin/presentation-testing


The transformer package @itwin/core-transformer was renamed to @itwin/imodel-transformer and has its own repository now with supporting packages.


@itwin/eslint-plugin has moved to the eslint-plugin repository.


@itwin/map-layers has moved into the viewer-components-react repository.

Deprecated API removals

The following previously-deprecated APIs have been removed:


  • AliCloudStorageService
  • AliCloudStorageServiceCredentials
  • AzureBlobStorage
  • CloudStorageService
  • CloudStorageTileUploader
  • CloudStorageUploadOptions
  • tileCacheService property of IModelHost, IModelHostOptions, and IModelHostConfiguration
  • IModelHost.tileUploader


  • CloudStorageCache
  • CloudStorageContainerDescriptor
  • CloudStorageContainerUrl
  • CloudStorageProvider
  • CloudStorageTileCache
  • IModelTileRpcInterface.getTileCacheContainerUrl
  • IModelTileRpcInterface.isUsingExternalTileCache


  • ContentInstancesOfSpecificClassesSpecification.handlePropertiesPolymorphically

Deprecated API replacements

Querying ECSql

ECSqlReader can be used as an AsyncIterableIterator. This makes migrating from using query to using createQueryReader much easier. Both of these are methods that exist in IModelDb, ECDb, and IModelConnection.

createQueryReader can now be used as shown below:

for await (const row of iModel.createQueryReader("SELECT * FROM bis.Element")) {
  const rowId = row[0]; // or 'row.id'

It is important to note that the object returned by createQueryReader is a QueryRowProxy object and not a raw JavaScript object. To get a raw JavaScript object (as would have been assumed previously when using query), call .toRow() on the QueryRowProxy object.

for await (const row of iModel.createQueryReader("SELECT * FROM bis.Element")) {
  const jsRow = row.toRow();

Interfaces changed




BackendHubAccess has been marked @internal from @beta. The 'hubAccess' property on IModelHostConfiguration has also been marked @internal from @beta.


Entity.getReferenceIds no longer returns a set of Id64String, but an EntityReferenceSet, because it now supports returning references of entities that aren't elements.


Mesh offset

The new static method PolyfaceQuery.cloneOffset creates a mesh with facets offset by a given distance. The image below illustrates the basic concepts.

Offset Example 1

At left is the original box, size 3 x 5 in the large face and 2 deep. The middle is constructed by cloneOffset with offset of 0.15 and default options. Note that it maintains the original sharp corners. The right box is constructed with OffsetMeshOptions.chamferAngleBetweenNormals of 80 degrees. This specifies that when the original angle between normals of adjacent facets exceeds 80 degrees the corner should be chamfered, creating the slender chamfer faces along the edges and the triangles at the vertices. The default 120 degree chamfer threshold encourages corners to be extended to intersection rather than chamfered.

The image below illustrates results with a more complex cross section.

Offset Example 2

The lower left is the original (smaller, inside) mesh with the (transparent) offset mesh around it with all sharp corners. At upper right the offset has chamfers, again due to setting the chamferAngleBetweenNormals to 120 degrees.

Mesh intersection with ray

New functionality computes the intersection(s) of a Ray3d with a Polyface. By default, PolyfaceQuery.intersectRay3d returns a FacetLocationDetail for the first found facet that intersects the infinite line parameterized by the ray. A callback can be specified in the optional FacetIntersectOptions parameter to customize intersection processing, e.g., to filter and collect multiple intersections. Other options control whether to populate the returned detail with interpolated auxiliary vertex data: normals, uv parameters, colors, and/or the barycentric scale factors used to interpolate such data.

There is also new support for intersecting a Ray3d with a triangle or a polygon. BarycentricTriangle.intersectRay3d and BarycentricTriangle.intersectSegment return a TriangleLocationDetail for the intersection point of the plane of the triangle with the infinite line parameterized by a ray or segment. Similarly, PolygonOps.intersectRay3d returns a PolygonLocationDetail for the intersection point in the plane of the polygon. Both returned detail objects contain properties classifying where the intersection point lies with respect to the triangle/polygon, including isInsideOrOn and closest edge data.

A new method Ray3d.intersectionWithTriangle is also added which is 2-3 times faster than BarycentricTriangle.intersectRay3d. This new method only returns the intersection coordinates of the ray and triangle and no extra data.

Abstract base class Plane3d

A new abstract base class Plane3d is defined to provide shared queries and enforce method names in multiple classes that act as 3D "planes" with various representations.

This will provide more consistency and functionality than previously provided by the interface PlaneAltitudeEvaluator. API compatibility with the weaker PlaneAltitudeEvaluator is maintained as follows:

  • The abstract base class Plane3d declares that it implements the PlaneAltitudeEvaluator.
  • Classes that extend Plane3d inherit the extended declaration of the base class (compatibility "by interface name").
  • Classes that extend Plane3d inherit the various abstract method obligations and (non-abstract) method implementations from the base class (compatibility "by collected list of methods").

With these changes the PlaneAltitudeEvaluator can be deprecated.

Intersect local ranges

A new method ClipUtilities.doLocalRangesIntersect is added for determining whether two Range3d objects in different local coordinates clash. This method performs an intersection of the ranges in the same coordinate system, without expanding their volumes, as can happen when a Range3d is rotated. An optional margin signed distance can be used to shrink or expand the second range before the intersection, allowing for proximity testing. This can be used, for example, to efficiently test whether two elements in an iModel are approximately within 50cm of each other:

  // first element data, e.g. from iModel query
  const range0 = Range3d.create(Point3d.fromJSON(el.bBoxLow), Point3d.fromJSON(el.bBoxHigh));
  const placement0 = Placement3d.fromJSON({ origin: el.origin, angles: { pitch: el.pitch, roll: el.roll, yaw: el.yaw } });
  // [...] second element data similarly
  const isClash = ClipUtilities.doLocalRangesIntersect(range0, placement0.transform, range1, placement1.transform, 0.5);


glTF bounding boxes

The existing readGltfGraphics function returns an opaque RenderGraphic. A new readGltf function has been added that produces a GltfGraphic that - in addition to the RenderGraphic - includes the bounding boxes of the glTF model in local and world coordinates.

Atmospheric Scattering

A physics-based Atmospheric Scattering effect is now available for the rendering system.

Globe View of Atmospheric Scattering

This effect can be toggled via Environment.displayAtmosphere and adjusted through Environment.atmosphere. It is also reactive to the sun's position defined at DisplayStyle3dSettings.lights.

The effect is only displayed with 3d geolocated iModels with DisplayStyleSettings.backgroundMap set to a backgroundMap with BackgroundMapSettings.globeMode equal to GlobeMode.Ellipsoid.

Sky View of Atmospheric Scattering Atmospheric Scattering from Space Atmospheric Scattering at Sunset

Constant LOD mapping mode

Constant level-of-detail ("LOD") mapping mode is a technique that dynamically calculates texture cordinates to keep the texture near a certain size on the screen, thus preserving the level of detail no matter what the zoom level. It blends from one size of the texture to another as the view is zoomed in or out so that the change is smooth.

You can create a RenderMaterial that uses this mode on the frontend via RenderSystem.createRenderMaterial by setting useConstantLod to true in MaterialTextureMappingProps and optionally specifying its parameters via constantLodProps (see TextureMapping.ConstantLodParamProps).

You can also have a normal map use constant LOD mapping by setting useConstantLod in its properties via MaterialTextureMappingProps.normalMapParams in your CreateRenderMaterialArgs.textureMapping. It is thus possible to have a pattern map which uses constant lod mapping and a normal map which uses some other texture mapping mode or visa versa.

To create a RenderMaterialElement with a constant LOD pattern map on the backend, use RenderMaterialElement.insert or RenderMaterialElement.create. Pass in a patternMap with a TextureMapProps which has pattern_useConstantLod set to true and optionally specify any or all of the pattern_constantLod_* properties.

To create a RenderMaterialElement with a constant LOD normal map on the backend, use RenderMaterialElement.insert or RenderMaterialElement.create. Pass the normal map in RenderMaterialElementParams.normalMap and turn on the useConstantLod flag in its NormalFlags property.

The image below illustrates the effects of constant LOD mapping.

Constant LOD mapping zoomin Constant LOD mapping


Active unit system

PresentationManager has a way to set active unit system either through props when initializing (PresentationManagerProps.activeUnitSystem) or directly through a setter (PresentationManager.activeUnitSystem). Both of these ways have been deprecated in favor of using QuantityFormatter.activeUnitSystem (access QuantityFormatter through IModelApp.quantityFormatter) to avoid asking consumers set the active unit system in two places. For the time being, while we keep the deprecated unit system setters on the presentation manager, they act as an override to QuantityFormatter.activeUnitSystem, but the latter is now used by default, so setting active unit system on presentation manager is not necessary any more.

Hierarchy level filtering and limiting

Two new features have been made available to help working with very large hierarchies - hierarchy level filtering and limiting. Filtering was already available since 3.6 and has been promoted to @beta, limiting has been newly added as @beta. See hierarchy filtering and limiting page for more details.

Stopped "eating" errors on the frontend

The PresentationManager used to "eat" errors and return default value instead of re-throwing and exposing them to consumers. This made it impossible for consumer code to know that an error occurred, which could cause it to make wrong decisions. The decision has been re-considered and now Presentation manager lets consumers catch the errors. This affects the following APIs:

Consumers of these APIs should make sure they're wrapped with try/catch blocks and the errors are handled appropriately. See our error handling page for more details.

Handling of long-running requests

The timeouts' strategy used for Presentation RPC has been changed.

Previously, the backend would return a "timeout" status if creating the response took more than 90 seconds (or as configured through PresentationPropsBase.requestTimeout). The frontend, upon receiving such a status, would repeat the request 5 times before propagating the timeout to the requestor on the frontend. This means that changing the timeout on the backend affects how long in total the frontend waits. By default that was 5 times 90 seconds, so 7.5 minutes in total.

Now, the two timeout configs on the backend and the frontend have been separated. The timeout on the frontend is set through PresentationManagerProps.requestTimeout and defaults to 10 minutes. Presentation manager will repeat the RPC request as many times as needed to wait at least 10 minutes until returning the "timeout" response to the requestor. With this change the timeout configuration on the backend becomes less important as it merely affects how often the frontend will have to repeat the request. It can still be changed through PresentationPropsBase.requestTimeout, but the default value has been reduced to 5 seconds.

Use content modifiers on nested content

Previously, the calculated and related properties defined in content modifiers were only applied on directly loaded instances' content. Occasionally, there is a need to request calculated and/or related properties to be loaded for specific instances in all situations, no matter if their content is loaded directly or indirectly. Previously that was only possible by chaining related properties and nested related properties attributes. Now the content modifier rule has an attribute applyOnNestedContent which indicates if the modifier should be used on nested content. This removes the need to have duplicate related properties specifications in those situations.

Dependency updates

In addition to upgrading iTwin.js core dependencies to 4.0, there are some other notable upgrades:

  • Support for React 18 (keep support of React 17 too).
  • Upgrade iTwinUI from v1 to v2.
  • @itwin/presentation-backend, @itwin/presentation-common and @itwin/presentation-frontend have new peer dependency @itwin/ecschema-metadata.


The deprecated field handleInstancesPolymorphically of ContentInstancesOfSpecificClassesSpecification has been removed. To specify handling polymorphically, specify the value in classes.arePolymorphic or excludedClasses.arePolymorphic.


Asynchronous schema loading

Added proper support for loading multiple schemas asynchronously and the ability to get information about a schema that is partially loaded.

const context = new SchemaContext();
const locater = new SchemaXmlFileLocater();

const schemaKey = new SchemaKey("MySchemaWithManyReferences", 1, 0, 42);

// Start loading the schema but return as soon as we have loaded the name and version of the schema and it's references
const schemaInfo = await context.getSchemaInfo(schemaKey, SchemaMatchType.Exact);
// Get the whole schema either awaiting the schema promise created by getSchemaInfo or start loading if not already started
const schema = await context.getSchema(schemaKey, SchemaMatchType.Exact);
// Await the schema promise created by getSchemaInfo or return undefined if not already started
const schema2 = await context.getCachedSchema(schemaKey, SchemaMatchType.Exact);

Other minor API changes

  • Added SchemaInfo interface with schema keys for a schema and it's references. Schema implicitly supports this interface.
  • Some beta components had breaking changes and were moved to internal:
    • SchemaGraph
      • Now supports working with a SchemaInfo and a SchemaContext necessitating the init be made async.
    • SchemaMap
      • Use Array<Schema> in its place.
    • SchemaCache
      • Updated to support caching partially loaded schemas, use SchemaContext to cache schemas in it's place.
  • Added helper method SchemaFileUtility.writeSchemaToXmlString to write schema xml to a string
  • Added Schema.startLoadingFromJson to partially load a schema and return as soon as the SchemaInfo could be loaded.

Last Updated: 22 May, 2023