5.12.0 Change Notes
- 5.12.0 Change Notes
Quantity formatting
Bearing and Azimuth formatting now respects the persistence unit's phenomenon
Previously, Bearing and Azimuth format types assumed the persisted magnitude was always a true azimuth (measured clockwise from north), regardless of the quantity's persistenceUnit. This was incorrect for properties whose persistenceUnit.phenomenon is Units.ANGLE (a raw mathematical angle, measured counter-clockwise from east) - see #9465.
Formatter.formatQuantity and Parser.parseQuantityString now branch on persistenceUnit.phenomenon:
Units.HORIZONTAL_DIRECTION(a phenomenon; e.g. itsUnits.HORIZONTAL_DIR_RADunit): unchanged - aHORIZONTAL_DIRECTIONvalue is already a true azimuth, so it's formatted/parsed as-is.Units.ANGLE(a phenomenon; e.g. itsUnits.RADunit): the90° − θconversion is now applied automatically before formatting anANGLEvalue, and inverse-applied after parsing one.
For code that persists Bearing/Azimuth values as ANGLE-phenomenon units and previously worked around the bug by manually applying its own 90° − θ correction: that manual correction must now be removed, or values will be double-converted. For example, AccuDraw's manual correction for its QuantityType.Angle bearing display (persisted as Units.RAD) has been removed as part of this change.
If your KindOfQuantity persists true azimuth values directly, switch its persistence unit to a Units.HORIZONTAL_DIRECTION unit (e.g. Units.HORIZONTAL_DIR_RAD) to opt out of the conversion entirely.
Note: if you switch your persistence unit's phenomenon, remember to also update revolutionUnit (and azimuthBaseUnit, if set) to a unit from the same phenomenon - e.g. Units.HORIZONTAL_DIR_REVOLUTION instead of Units.REVOLUTION for a Units.HORIZONTAL_DIRECTION persistence unit. These units cannot be converted across phenomena, so a mismatch will fail to resolve. See Bearing and Azimuth Format for details.
Electron
Electron 43 support
In addition to already supported Electron versions, iTwin.js now supports Electron 43.
Backend-to-frontend IPC invoke
For apps with a dedicated backend, the backend can now invoke methods on the frontend and receive a return value, mirroring the existing frontend-to-backend pattern. Previously IpcHost could only send one-way messages to the frontend; the reverse request/response direction had no equivalent of IpcSocketFrontend.invoke.
The new @beta APIs are:
IpcHost.invokeandIpcHost.makeIpcProxyon the backend to call frontend handlers.IpcApp.handleand a newIpcHandlerbase class on the frontend to implement them.
Because Electron provides no native main-to-renderer invoke (only one-way webContents.send), this is implemented on top of the existing send/addListener primitives, so it works over both the Electron IPC and web socket transports (mobile included, since it runs over web sockets).
Pending invocations are rejected if IpcHost.shutdown is called before a response arrives, so promises never leak past shutdown.
When a frontend handler throws, the error is surfaced to the backend caller following the ITwinError paradigm: it is rebuilt as an Error preserving the message, iTwinErrorId, error number, logging metadata, and any custom properties, so the caller can identify it with ITwinError.isError (or BentleyError.isError for legacy error numbers) rather than relying on a class identity that cannot survive marshalling across the Ipc boundary. A non-BentleyError (e.g. a plain Error) is re-thrown with its message and any own-enumerable properties preserved. (The existing frontend-to-backend direction continues to rethrow a backend BentleyError as the pre-existing BackendError for backwards compatibility.)
@itwin/core-backend
ChangesetReader.setBatchSize
ChangesetReader now exposes a setBatchSize(n: number) method that controls how many change rows are cached in the reader. It is a performance improvement parameter that can be tweaked as per user's choice. Increasing the batch size increases the number of rows read at once and cached in the reader, thereby improving throughput when iterating large changesets but it also increases memory consumption; decreasing it reduces peak memory use. The method must be called before the first ChangesetReader.step call.
Default batch sizes (unchanged behaviour when setBatchSize is not called):
| Active configuration | Default |
|---|---|
propFilter: InstanceKey |
100 |
propFilter: BisCoreElement |
20 |
propFilter: All, abbreviateBlobs: false |
5 |
propFilter: All (blobs abbreviated or unset) |
10 |
Performance improvement with new caching behaviour in ChangesetReader`:
| Cache type | Inserts | Before (s) | After (s) | Improvement |
|---|---|---|---|---|
| InMemoryCache | 1,000 | 0.220 | 0.204 | 7.3% |
| InMemoryCache | 10,000 | 2.213 | 1.402 | 36.6% |
| SqliteBackedCache | 1,000 | 0.399 | 0.207 | 48.1% |
| SqliteBackedCache | 10,000 | 3.342 | 1.981 | 40.7% |
@itwin/core-geometry
Region Boolean enhancements
RegionOps.regionBooleanXY has improved simplification and new options in the RegionBooleanXYOptions options bundle:
| Option name | Type | Default Value | Description |
|---|---|---|---|
simplifyUnion |
boolean | false |
When true, holes are now preserved. |
operationGroupA |
RegionBinaryOpType | RegionBinaryOpType.Union |
Operation to apply to the regions of the first input argument. |
operationGroupB |
RegionBinaryOpType | RegionBinaryOpType.Union |
Operation to apply to the regions of the second input argument. |
RegionBooleanXYOptions.simplifyUnion
The default behavior of RegionOps.regionBooleanXY results in UnionRegions with algorithmically inserted bridge edges removed, but with other interior edges remaining. The previous true behavior for this option applied only to RegionBinaryOpType.Union operations, and simplified the output by returning only the outer loop, but at the cost of losing all implied holes, which was less than desirable. Now passing simplifyUnion: true not only removes interior edges, but also preserves holes, and returns the simplest region type for all operations, not just unions.
For example, consider the union of four trapezoids to form a "picture frame". The following call produces a (rather naive!) UnionRegion in which the four input Loops survive as children, and the hole is only implied---it cannot be queried:

When we pass simplifyUnion: true, the result is now a ParityRegion. This simpler output not only lacks extraneous interior edges, but also explicitly captures the outer and hole Loops as children:

RegionBooleanXYOptions.operationGroupA/B
The previous/default behavior of RegionOps.regionBooleanXY assumes an implicit union of the regions in each input group. With these new options, you can now specify intersection and parity operations to be performed on the regions in each group, before the main Boolean operation is performed on the groups.
So for example, consider subtracting the intersection of a 4-loop (green) Venn diagram's inner region from an outer (red) loop:

Before the new options, you would have to call this method 4 times: 3 pairwise Boolean intersections among the Venn loops, and a Boolean difference. Now you can compute the ParityRegion result all in one go:

Note: The same result can also be obtained with RegionBinaryOpType.BMinusA instead of RegionBinaryOpType.Parity. To perform only the 4-way intersection, pass undefined for the second input group.
@itwin/ecschema-metadata
SchemaView: load only a subset of schemas, cheaper cache invalidation
SchemaView got two changes to improve performance on iModels with very large schemas.
getSchemaView() on IModelDb and IModelConnection now takes an optional schemas argument to load only a subset:
The returned view accumulates: a later call with different schemas merges their reference closure into the same view, so schemas loaded by an earlier call stay available, and once the full set is loaded (via a call with no filter), every subsequent call is a synchronous no-op.
Second, cache invalidation - detecting whether an iModel's schemas changed since a view was cached - switched from hashing the full contents of every schema table to hashing only each schema's name and version. On a large iModel (~30 GB, ~100 schemas), this dropped the check from over a second of CPU time to about a millisecond. The one accepted limitation: a schema whose content changes without a version bump is not detected. This can only happen with dynamic schemas, since ECDb requires a version increment for in-place re-import of any other schema.
Last Updated: 03 August, 2026