@itwin/core-common
Version:
iTwin.js components common to frontend and backend
1,015 lines (1,014 loc) • 57.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module DisplayStyles
*/
import { assert, compareBooleans, compareNumbers, comparePossiblyUndefined, compareStrings, compareStringsOrUndefined, CompressedId64Set, Id64, OrderedId64Iterable, } from "@itwin/core-bentley";
import { ClipPlane, ClipPrimitive, ClipVector, ConvexClipPlaneSet, Matrix3d, Plane3dByOriginAndUnitNormal, Point3d, Point4d, Range1d, Transform, UnionOfConvexClipPlaneSets, Vector3d, } from "@itwin/core-geometry";
import { RgbColor } from "./RgbColor";
import { FeatureAppearance } from "./FeatureSymbology";
function interpolate(start, end, fraction) {
return start + fraction * (end - start);
}
function interpolateRgb(start, end, fraction) {
return new RgbColor(interpolate(start.r, end.r, fraction), interpolate(start.g, end.g, fraction), interpolate(start.b, end.b, fraction));
}
function compareXYZ(lhs, rhs) {
return compareNumbers(lhs.x, rhs.x) || compareNumbers(lhs.y, rhs.y) || compareNumbers(lhs.z, rhs.z);
}
function compare4d(lhs, rhs) {
return compareNumbers(lhs.x, rhs.x) || compareNumbers(lhs.y, rhs.y) || compareNumbers(lhs.z, rhs.z) || compareNumbers(lhs.w, rhs.w);
}
const scratchVec3a = new Vector3d();
const scratchVec3b = new Vector3d();
function compareMatrices(lhs, rhs) {
return compareXYZ(lhs.columnX(scratchVec3a), rhs.columnX(scratchVec3b))
|| compareXYZ(lhs.columnY(scratchVec3a), rhs.columnY(scratchVec3b))
|| compareXYZ(lhs.columnZ(scratchVec3a), rhs.columnZ(scratchVec3b));
}
function compareDurations(lhs, rhs) {
return compareNumbers(lhs.low, rhs.low) || compareNumbers(lhs.high, rhs.high);
}
/** Namespace containing types that collectively define a script that animates the contents of a view by adjusting the visibility, position,
* and/or symbology of groups of elements over time. A [[RenderSchedule.Script]] is hosted by a [RenderTimeline]($backend) element. The script
* can be associated with a [DisplayStyleState]($frontend) by way of its [[DisplayStyleSettings.renderTimeline]] property.
* @public
*/
export var RenderSchedule;
(function (RenderSchedule) {
/** Defines how two interpolate between two entries in a [[RenderSchedule.Timeline]].
* @note Currently only Linear and Step are supported. Any other value is treated as Step.
* @see [[RenderSchedule.TimelineEntry]].
*/
let Interpolation;
(function (Interpolation) {
/** Each timeline entry's value is discrete - the timeline jumps from one entry's value directly to another. */
Interpolation[Interpolation["Step"] = 1] = "Step";
/** Given two entries on the timeline and a timepoint in between them, linearly interpolate based on the timepoint's distance between the
* two entries.
*/
Interpolation[Interpolation["Linear"] = 2] = "Linear";
})(Interpolation = RenderSchedule.Interpolation || (RenderSchedule.Interpolation = {}));
/** Describes the value of some property at a specific point along a [[RenderSchedule.Timeline]].
* @see [[RenderSchedule.VisibilityEntry]]
* @see [[RenderSchedule.ColorEntry]]
* @see [[RenderSchedule.TransformEntry]]
* @see [[RenderSchedule.CuttingPlaneEntry]]
*/
class TimelineEntry {
/** The time point in seconds in the [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time). */
time;
/** How to interpolate from this entry to the next entry in the timeline. */
interpolation;
constructor(props) {
this.time = props.time;
this.interpolation = props.interpolation === Interpolation.Linear ? props.interpolation : Interpolation.Step;
}
toJSON() {
const props = {
time: this.time,
};
if (this.interpolation === Interpolation.Linear)
props.interpolation = this.interpolation;
return props;
}
compareTo(other) {
return compareNumbers(this.interpolation, other.interpolation) || compareNumbers(this.time, other.time);
}
equals(other) {
return 0 === this.compareTo(other);
}
}
RenderSchedule.TimelineEntry = TimelineEntry;
/** A timeline entry that controls the visibility of the associated geometry. */
class VisibilityEntry extends TimelineEntry {
/** The visibility of the geometry at this point on the timeline, in the range [0, 100] where 0 is completely invisible, 100 is completely visible,
* and values in between indicate increasing opacity.
*/
value;
constructor(props) {
super(props);
if (typeof props.value !== "number")
this.value = 100;
else
this.value = Math.max(0, Math.min(100, props.value));
}
toJSON() {
const props = super.toJSON();
if (100 !== this.value)
props.value = this.value;
return props;
}
compareTo(other) {
assert(other instanceof VisibilityEntry);
return super.compareTo(other) || compareNumbers(this.value, other.value);
}
}
RenderSchedule.VisibilityEntry = VisibilityEntry;
/** A timeline entry controlling the color of the affected geometry. */
class ColorEntry extends TimelineEntry {
/** If defined, the color in which to draw the geometry. If undefined, the geometry is drawn in its actual color. */
value;
constructor(props) {
super(props);
if (props.value)
this.value = new RgbColor(props.value.red, props.value.green, props.value.blue);
}
toJSON() {
const props = super.toJSON();
if (this.value) {
props.value = {
red: this.value.r,
green: this.value.g,
blue: this.value.b,
};
}
return props;
}
compareTo(other) {
assert(other instanceof ColorEntry);
return super.compareTo(other) || comparePossiblyUndefined((lhs, rhs) => lhs.compareTo(rhs), this.value, other.value);
}
}
RenderSchedule.ColorEntry = ColorEntry;
/** Describes the components of a [[RenderSchedule.TransformEntry]] as a rotation around a pivot point followed by a translation. */
class TransformComponents {
/** Pivot point - applied before rotation. */
pivot;
/** Quaternion rotation. */
orientation;
/** Translation - applied after rotation. */
position;
constructor(position, pivot, orientation) {
this.position = position;
this.pivot = pivot;
this.orientation = orientation;
}
static fromJSON(props) {
if (props.pivot && props.position && props.orientation)
return new TransformComponents(Vector3d.fromJSON(props.position), Vector3d.fromJSON(props.pivot), Point4d.fromJSON(props.orientation));
else
return undefined;
}
toJSON() {
return {
position: [this.position.x, this.position.y, this.position.z],
pivot: [this.pivot.x, this.pivot.y, this.pivot.z],
orientation: [this.orientation.x, this.orientation.y, this.orientation.z, this.orientation.w],
};
}
compareTo(other) {
return compareXYZ(this.pivot, other.pivot) || compareXYZ(this.position, other.position) || compare4d(this.orientation, other.orientation);
}
equals(other) {
return this.pivot.isAlmostEqual(other.pivot) && this.position.isAlmostEqual(other.position) && this.orientation.isAlmostEqual(other.orientation);
}
}
RenderSchedule.TransformComponents = TransformComponents;
/** A timeline entry that applies rotation, scaling, and/or translation to the affected geometry. */
class TransformEntry extends TimelineEntry {
/** The transform matrix to be applied to the geometry, used only if [[components]] is not defined. */
value;
/** The transform represented as a rotation about a pivot point followed by a translation. If undefined, [[value]] is used instead. */
components;
constructor(props) {
super(props);
this.value = props.value ? Transform.fromJSON(props.value.transform) : Transform.identity;
if (props.value)
this.components = TransformComponents.fromJSON(props.value);
}
toJSON() {
const props = super.toJSON();
if (this.components) {
props.value = this.components.toJSON();
props.value.transform = this.value.toRows();
}
else {
props.value = { transform: this.value.toRows() };
}
return props;
}
compareTo(other) {
assert(other instanceof TransformEntry);
const cmp = super.compareTo(other);
if (0 !== cmp)
return cmp;
if (this.components || other.components) {
if (!this.components || !other.components)
return this.components ? 1 : -1;
return this.components.compareTo(other.components);
}
return compareXYZ(this.value.origin, other.value.origin) || compareMatrices(this.value.matrix, other.value.matrix);
}
}
RenderSchedule.TransformEntry = TransformEntry;
/** Defines a [ClipPlane]($core-geometry) associated with a [[RenderSchedule.CuttingPlaneEntry]]. */
class CuttingPlane {
/** A point on the plane. */
position;
/** The direction perpendicular to the plane pointing toward the clip. */
direction;
/** If true, the clip plane is ignored and the geometry is never clipped. */
visible;
/** If true, the clip plane is ignored and the geometry is always clipped. */
hidden;
constructor(props) {
this.position = Point3d.fromJSON(props.position);
this.direction = Point3d.fromJSON(props.direction);
this.hidden = true === props.hidden;
this.visible = true === props.visible;
}
toJSON() {
const props = {
position: [this.position.x, this.position.y, this.position.z],
direction: [this.direction.x, this.direction.y, this.direction.z],
};
if (this.visible)
props.visible = true;
if (this.hidden)
props.hidden = true;
return props;
}
compareTo(other) {
return compareXYZ(this.position, other.position) || compareXYZ(this.direction, other.direction) || compareBooleans(this.visible, other.visible) || compareBooleans(this.hidden, other.hidden);
}
equals(other) {
return 0 === this.compareTo(other);
}
}
RenderSchedule.CuttingPlane = CuttingPlane;
/** A timeline entry that applies a [ClipPlane]($core-geometry) to the affected geometry. */
class CuttingPlaneEntry extends TimelineEntry {
/** The definition of the [ClipPlane]($core-geometry), or undefined if this entry applies no clipping. */
value;
constructor(props) {
super(props);
if (props.value)
this.value = new CuttingPlane(props.value);
}
toJSON() {
const props = super.toJSON();
if (this.value)
props.value = this.value.toJSON();
return props;
}
compareTo(other) {
assert(other instanceof CuttingPlaneEntry);
return super.compareTo(other) || comparePossiblyUndefined((x, y) => x.compareTo(y), this.value, other.value);
}
}
RenderSchedule.CuttingPlaneEntry = CuttingPlaneEntry;
/** Identifies a fractional position along a [[RenderSchedule.Timeline]] between any two [[RenderSchedule.TimelineEntry]]'s within a [[RenderSchedule.TimelineEntryList]].
* @internal
*/
class Interval {
/** The index of the first timeline entry within the list. */
lowerIndex;
/** The index of the second timeline entry within the list. */
upperIndex;
/** The normalized distance between the two timeline entries. */
fraction;
constructor(lower = 0, upper = 0, fraction = 0) {
this.init(lower, upper, fraction);
}
init(lower = 0, upper = 0, fraction = 0) {
this.lowerIndex = lower;
this.upperIndex = upper;
this.fraction = fraction;
}
}
RenderSchedule.Interval = Interval;
/** A list of the [[RenderSchedule.TimelineEntry]] objects within a [[RenderSchedule.Timeline]]. The type parameters are:
* - T, a subclass of TimelineEntry with a `value` property specifying the value of the property controlled by the timeline at that entry's time point.
* - P, the JSON representation from which T is to be constructed.
* - V, the type of `T.value`.
*/
class TimelineEntryList {
_entries;
/** The total time period represented by the entries in this list. */
duration;
constructor(props, ctor) {
this.duration = Range1d.createNull();
this._entries = props.map((x) => {
const entry = new ctor(x);
this.duration.extendX(entry.time);
return entry;
});
}
/** The number of entries in the list. */
get length() {
return this._entries.length;
}
/** An iterator over the entries in the list. */
[Symbol.iterator]() {
return this._entries[Symbol.iterator]();
}
/** Look up an entry by its position in the list. */
getEntry(index) {
return this._entries[index];
}
/** Look up the value of an entry by its position in the list. */
getValue(index) {
return this.getEntry(index)?.value;
}
toJSON() {
return this._entries.map((x) => x.toJSON());
}
compareTo(other) {
let cmp = compareNumbers(this._entries.length, other._entries.length) || compareDurations(this.duration, other.duration);
if (0 === cmp) {
for (let i = 0; i < this.length; i++)
if (0 !== (cmp = this._entries[i].compareTo(other._entries[i])))
break;
}
return cmp;
}
equals(other) {
return 0 === this.compareTo(other);
}
/** @internal */
findInterval(time, interval) {
if (this.length === 0)
return undefined;
interval = interval ?? new Interval();
if (time < this._entries[0].time) {
interval.init(0, 0, 0);
return interval;
}
const last = this.length - 1;
if (time >= this._entries[last].time) {
interval.init(last, last, 0);
return interval;
}
for (let i = 0; i < last; i++) {
const time0 = this._entries[i].time;
const time1 = this._entries[i + 1].time;
if (time0 <= time && time1 >= time) {
let fraction;
if (Interpolation.Linear === this._entries[i].interpolation)
fraction = (time - time0) / (time1 - time0);
else
fraction = 0;
interval.init(i, i + 1, fraction);
return interval;
}
}
return undefined;
}
}
RenderSchedule.TimelineEntryList = TimelineEntryList;
const scratchInterval = new Interval();
/** A list of [[RenderSchedule.VisibilityEntry]]s within a [[RenderSchedule.Timeline]]. */
class VisibilityTimelineEntries extends TimelineEntryList {
/** Returns the visibility value for the entry at the specified position in the list, or 100 (fully-visible) if no such entry exists. */
getValue(index) {
return super.getValue(index) ?? 100;
}
}
RenderSchedule.VisibilityTimelineEntries = VisibilityTimelineEntries;
/** A list of [[RenderSchedule.TransformEntry]]s within a [[RenderSchedule.Timeline]]. */
class TransformTimelineEntries extends TimelineEntryList {
/** Returns the transform for the entry at the specified position in the list, or an identity transform if no such entry exists. */
getValue(index) {
return super.getValue(index) ?? Transform.identity;
}
}
RenderSchedule.TransformTimelineEntries = TransformTimelineEntries;
/** Specifies how to animate a set of geometry over time within a [[RenderSchedule.Script]].
* A [[RenderSchedule.Script]] can contain any number of [[RenderSchedule.Timeline]]s, each affecting different sets of geometry.
* @see [[RenderSchedule.ElementTimeline]] and [[RenderSchedule.ModelTimeline]].
*/
class Timeline {
/** Sequence controlling the visibility of the geometry. */
visibility;
/** Sequence controlling the color of the geometry. */
color;
/** Sequence controlling the position, orientation, and/or scale of the geometry. */
transform;
/** Sequence controlling how the geometry is clipped. */
cuttingPlane;
/** The total time period represented by this timeline. */
duration;
/** Indicates whether the schedule editing session has been finalized and is no longer active.
* @internal
*/
isEditingCommitted = false;
constructor(props) {
this.duration = Range1d.createNull();
if (props.visibilityTimeline) {
this.visibility = new VisibilityTimelineEntries(props.visibilityTimeline, VisibilityEntry);
this.duration.extendRange(this.visibility.duration);
}
if (props.colorTimeline) {
this.color = new TimelineEntryList(props.colorTimeline, ColorEntry);
this.duration.extendRange(this.color.duration);
}
if (props.transformTimeline) {
this.transform = new TransformTimelineEntries(props.transformTimeline, TransformEntry);
this.duration.extendRange(this.transform.duration);
}
if (props.cuttingPlaneTimeline) {
this.cuttingPlane = new TimelineEntryList(props.cuttingPlaneTimeline, CuttingPlaneEntry);
this.duration.extendRange(this.cuttingPlane.duration);
}
}
toJSON() {
return {
visibilityTimeline: this.visibility?.toJSON(),
colorTimeline: this.color?.toJSON(),
transformTimeline: this.transform?.toJSON(),
cuttingPlaneTimeline: this.cuttingPlane?.toJSON(),
};
}
compareTo(other) {
const cmp = compareDurations(this.duration, other.duration);
if (0 !== cmp)
return cmp;
// Do cheaper checks before iterating through timeline entries
if (!!this.visibility !== !!other.visibility)
return this.visibility ? 1 : -1;
else if (!!this.color !== !!other.color)
return this.color ? 1 : -1;
else if (!!this.transform !== !!other.transform)
return this.transform ? 1 : -1;
else if (!!this.cuttingPlane !== !!other.cuttingPlane)
return this.cuttingPlane ? 1 : -1;
return comparePossiblyUndefined((x, y) => x.compareTo(y), this.visibility, other.visibility) || comparePossiblyUndefined((x, y) => x.compareTo(y), this.color, other.color)
|| comparePossiblyUndefined((x, y) => x.compareTo(y), this.transform, other.transform) || comparePossiblyUndefined((x, y) => x.compareTo(y), this.cuttingPlane, other.cuttingPlane);
}
equals(other) {
return 0 === this.compareTo(other);
}
/** Get the visibility of the geometry at the specified time point. */
getVisibility(time) {
let interval;
if (!this.visibility || !(interval = this.visibility.findInterval(time, scratchInterval)))
return 100;
let visibility = this.visibility.getValue(interval.lowerIndex) ?? 100;
if (interval.fraction > 0)
visibility = interpolate(visibility, this.visibility.getValue(interval.upperIndex) ?? 100, interval.fraction);
return visibility;
}
/** Get the color of the geometry at the specified time point, or undefined if the color is not overridden at that time point. */
getColor(time) {
let interval;
if (!this.color || !(interval = this.color.findInterval(time, scratchInterval)))
return undefined;
const start = this.color.getValue(interval.lowerIndex);
if (start && interval.fraction > 0) {
const end = this.color.getValue(interval.upperIndex);
if (end)
return interpolateRgb(start, end, interval.fraction);
}
return start;
}
/** Get the transform applied to the geometry at the specified time point. */
getAnimationTransform(time) {
let interval;
if (!this.transform || !(interval = this.transform.findInterval(time, scratchInterval)))
return Transform.identity;
let transform = this.transform.getValue(interval.lowerIndex);
if (interval.fraction > 0) {
const comp0 = this.transform.getEntry(interval.lowerIndex)?.components;
const comp1 = this.transform.getEntry(interval.upperIndex)?.components;
if (comp0 && comp1) {
const sum = Point4d.interpolateQuaternions(comp0.orientation, interval.fraction, comp1.orientation);
const matrix = Matrix3d.createFromQuaternion(sum);
const pre = Transform.createTranslation(comp0.pivot);
const post = Transform.createTranslation(comp0.position.interpolate(interval.fraction, comp1.position));
const product = post.multiplyTransformMatrix3d(matrix);
product.multiplyTransformTransform(pre, product);
transform = product;
}
else {
const end = this.transform.getValue(interval.upperIndex);
const q0 = transform.matrix.inverse()?.toQuaternion();
const q1 = end.matrix.inverse()?.toQuaternion();
if (q0 && q1) {
const sum = Point4d.interpolateQuaternions(q0, interval.fraction, q1);
const matrix = Matrix3d.createFromQuaternion(sum);
const origin0 = Vector3d.createFrom(transform.origin);
const origin1 = Vector3d.createFrom(end.origin);
transform = Transform.createRefs(origin0.interpolate(interval.fraction, origin1), matrix);
}
}
}
return transform;
}
/** Get the clipping plane applied to the geometry at the specified time point, or undefined if the geometry is unclipped at that time point. */
getCuttingPlane(time) {
let interval;
if (!this.cuttingPlane || !(interval = this.cuttingPlane.findInterval(time, scratchInterval)))
return undefined;
const start = this.cuttingPlane.getValue(interval.lowerIndex);
if (!start)
return undefined;
const position = Point3d.createFrom(start.position);
const direction = Vector3d.createFrom(start.direction);
const end = interval.fraction > 0 ? this.cuttingPlane.getValue(interval.upperIndex) : undefined;
if (end) {
position.interpolate(interval.fraction, end.position, position);
direction.interpolate(interval.fraction, end.direction, direction);
}
else {
if (start.hidden || start.visible)
return undefined;
}
direction.negate(direction);
direction.normalizeInPlace();
return Plane3dByOriginAndUnitNormal.create(position, direction);
}
/** Create a ClipVector from the [[RenderSchedule.CuttingPlane]] applied to the geometry at the specified time point, if any. */
getClipVector(time) {
const plane = this.getCuttingPlane(time);
if (!plane)
return undefined;
const cp = ClipPlane.createPlane(plane);
const cps = UnionOfConvexClipPlaneSets.createConvexSets([ConvexClipPlaneSet.createPlanes([cp])]);
const prim = ClipPrimitive.createCapture(cps);
return ClipVector.createCapture([prim]);
}
/** @internal */
getFeatureAppearance(visibility, time) {
const transparency = visibility < 100 ? (1 - visibility / 100) : undefined;
const rgb = this.getColor(time);
return undefined !== rgb || undefined !== transparency ? FeatureAppearance.fromJSON({ rgb, transparency }) : undefined;
}
}
RenderSchedule.Timeline = Timeline;
/** Specifies how to animate the geometry belonging to a set of [GeometricElement]($backend)s as part of a [[RenderSchedule.Script]]. */
class ElementTimeline extends Timeline {
/** A positive integer that uniquely identififes this timeline among all ElementTimelines in the [[RenderSchedule.Script]]. */
batchId;
_elementIds;
constructor(props) {
super(props);
this.batchId = props.batchId;
this._elementIds = props.elementIds;
}
static fromJSON(props) {
return new ElementTimeline(props ?? { elementIds: [], batchId: 0 });
}
toJSON() {
return {
...super.toJSON(),
batchId: this.batchId,
elementIds: this._elementIds,
};
}
get containsElementIds() {
return this._elementIds.length > 0;
}
compareElementIds(other) {
if (typeof this._elementIds === typeof other._elementIds) {
const cmp = compareNumbers(this._elementIds.length, other._elementIds.length);
if (0 !== cmp)
return cmp;
if (typeof this._elementIds === "string") {
assert(typeof other._elementIds === "string");
return compareStrings(this._elementIds, other._elementIds);
}
}
// One or both are stored as arrays, in which case they might contain the same Ids in different orders. We will consider them different in that case.
const mine = this.elementIds[Symbol.iterator]();
const theirs = other.elementIds[Symbol.iterator]();
while (true) {
const a = mine.next();
const b = theirs.next();
if (a.done !== b.done)
return compareBooleans(!!a.done, !!b.done);
else if (a.done)
return 0;
const cmp = compareStrings(a.value, b.value);
if (0 !== cmp)
return cmp;
}
}
compareTo(other) {
assert(other instanceof ElementTimeline);
return compareNumbers(this.batchId, other.batchId) || this.compareElementIds(other) || super.compareTo(other);
}
/** @internal */
static getElementIds(ids) {
if (typeof ids === "string")
return CompressedId64Set.iterable(ids);
else if (Array.isArray(ids)) {
return ids;
}
else {
return [];
}
}
/** The Ids of the elements controlled by this timeline. */
get elementIds() {
return ElementTimeline.getElementIds(this._elementIds);
}
/** True if this timeline affects the color or transparency of the elements. */
get containsFeatureOverrides() {
return undefined !== this.visibility || undefined !== this.color;
}
/** If true, applying this timeline requires special tiles to be generated in which groups of elements are batched into nodes.
* @internal
*/
get requiresBatching() {
if (this.cuttingPlane)
return true;
return this.batchId !== 0 && (undefined !== this.color || undefined !== this.visibility);
}
/** True if this timeline affects the position, orientation, or scale of the elements. */
get containsTransform() {
return undefined !== this.transform;
}
/** @internal */
addSymbologyOverrides(overrides, time) {
assert(0 !== this.batchId);
const vis = this.getVisibility(time);
if (vis <= 0) {
overrides.setAnimationNodeNeverDrawn(this.batchId);
return;
}
const appearance = this.getFeatureAppearance(vis, time);
if (appearance)
overrides.overrideAnimationNode(this.batchId, appearance);
}
}
RenderSchedule.ElementTimeline = ElementTimeline;
/** Specifies how to animate the geometry within a [GeometricModel]($backend) as part of a [[RenderSchedule.Script]]. */
class ModelTimeline extends Timeline {
/** The Id of the [GeometricModel]($backend) to be animated. */
modelId;
/** @internal */
realityModelUrl;
/** Timelines specifying how to animate groups of [GeometricElement]($backend)s within the model. */
elementTimelines;
/** @internal */
transformBatchIds;
/** True if this timeline affects the color or transparency of the geometry. */
containsFeatureOverrides;
/** True if this timeline applies clipping to the model. */
containsModelClipping;
/** If true, applying this timeline requires special tiles to be generated in which groups of elements are batched into nodes.
* @internal
*/
requiresBatching;
/** True if this timeline affects the position, orientation, or scale of the geometry. */
containsTransform;
/** True if any [[RenderSchedule.ElementTimeline]]s exist and none of them contain any element Ids. This generally indicates that
* the backend was instructed to omit the Ids to save space when supplying the script.
*/
omitsElementIds;
_maxBatchId;
/** Tile tree suppliers perform **very** frequent ordered comparisons of ModelTimelines. They need to be fast. */
_cachedComparisons = new WeakMap();
/** When loading tiles we need to quickly map element Ids to batch Ids. This map is initialized on first call to [[getTimelineForElement]] to facilitate that lookup. */
_idPairToElementTimeline;
_discreteBatchIds;
constructor(props) {
super(props);
this.modelId = props.modelId;
this.realityModelUrl = props.realityModelUrl;
this.containsModelClipping = undefined !== this.cuttingPlane;
let containsFeatureOverrides = undefined !== this.visibility || undefined !== this.color;
let requiresBatching = false;
let containsTransform = false;
const transformBatchIds = [];
const elementTimelines = [];
let containsElementIds = false;
for (const elProps of props.elementTimelines) {
const el = ElementTimeline.fromJSON(elProps);
elementTimelines.push(el);
this.duration.extendRange(el.duration);
if (el.containsTransform) {
containsTransform = true;
if (el.batchId)
transformBatchIds.push(el.batchId);
}
containsFeatureOverrides ||= el.containsFeatureOverrides;
requiresBatching ||= el.requiresBatching;
containsElementIds = containsElementIds || el.containsElementIds;
}
this.elementTimelines = elementTimelines;
this.transformBatchIds = transformBatchIds;
this.omitsElementIds = elementTimelines.length > 0 && !containsElementIds;
this.containsFeatureOverrides = containsFeatureOverrides;
this.requiresBatching = requiresBatching;
this.containsTransform = containsTransform;
}
static fromJSON(props) {
return new ModelTimeline(props ?? { elementTimelines: [], modelId: Id64.invalid });
}
toJSON() {
return {
...super.toJSON(),
modelId: this.modelId,
realityModelUrl: this.realityModelUrl,
elementTimelines: this.elementTimelines.map((x) => x.toJSON()),
};
}
compareTo(other) {
if (this === other)
return 0;
const cached = this._cachedComparisons.get(other);
if (undefined !== cached)
return cached;
assert(other instanceof ModelTimeline);
let cmp = compareStrings(this.modelId, other.modelId) || compareStringsOrUndefined(this.realityModelUrl, other.realityModelUrl)
|| compareNumbers(this.elementTimelines.length, other.elementTimelines.length) || compareBooleans(this.containsFeatureOverrides, other.containsFeatureOverrides)
|| compareBooleans(this.containsModelClipping, other.containsModelClipping) || compareBooleans(this.containsTransform, other.containsTransform)
|| super.compareTo(other);
if (0 === cmp) {
for (let i = 0; i < this.elementTimelines.length; i++)
if (0 !== (cmp = this.elementTimelines[i].compareTo(other.elementTimelines[i])))
break;
}
this._cachedComparisons.set(other, cmp);
other._cachedComparisons.set(this, -cmp);
return cmp;
}
/** Look up the element timeline with the specified batch Id. */
findByBatchId(batchId) {
return this.elementTimelines.find((x) => x.batchId === batchId);
}
/** @internal */
addSymbologyOverrides(overrides, time) {
const appearance = this.getFeatureAppearance(this.getVisibility(time), time);
if (appearance)
overrides.override({ modelId: this.modelId, appearance });
for (const timeline of this.elementTimelines)
timeline.addSymbologyOverrides(overrides, time);
}
/** Obtain the transform applied to the model at the specified time point, if any. */
getTransform(batchId, time) {
return this.findByBatchId(batchId)?.getAnimationTransform(time);
}
/** Get the highest batchId of any ElementTimeline in this timeline. */
get maxBatchId() {
if (undefined === this._maxBatchId) {
this._maxBatchId = 0;
for (const elem of this.elementTimelines)
this._maxBatchId = Math.max(this._maxBatchId, elem.batchId);
}
return this._maxBatchId;
}
/** Given the two halves of an [Id64]($bentley) return the [[ElementTimeline]] containing the corresponding element.
* @note The first call to this method populates a mapping for fast lookup.
* @alpha
*/
getTimelineForElement(idLo, idHi) {
if (!this._idPairToElementTimeline) {
this._idPairToElementTimeline = new Id64.Uint32Map();
for (const timeline of this.elementTimelines) {
for (const elementId of timeline.elementIds) {
// NB: a malformed script may place the same element Id into multiple timelines. We're not going to check for such data errors here.
this._idPairToElementTimeline.setById(elementId, timeline);
}
}
}
return this._idPairToElementTimeline.get(idLo, idHi);
}
/** The batch Ids of the subset of [[elementTimelines]] that apply a transform and/or cutting plane.
* @alpha
*/
get discreteBatchIds() {
if (!this._discreteBatchIds) {
this._discreteBatchIds = new Set(this.transformBatchIds);
for (const timeline of this.elementTimelines)
if (!timeline.containsTransform && undefined !== timeline.cuttingPlane)
this._discreteBatchIds.add(timeline.batchId);
}
return this._discreteBatchIds;
}
/** @internal see ImdlReader.readAnimationBranches */
getBatchIdForFeature(feature) {
assert(Id64.fromUint32PairObject(feature.modelId) === this.modelId);
const elementTimeline = this.getTimelineForElement(feature.elementId.lower, feature.elementId.upper);
return elementTimeline?.batchId ?? 0;
}
}
RenderSchedule.ModelTimeline = ModelTimeline;
/** Specifies how to animate the contents of a [ViewState]($frontend) over time. The script contains any number of [[RenderSchedule.ModelTimeline]]s, each describing how
* to animate one of the models in the view.
* @see [RenderTimeline]($backend) to create an [Element]($backend) to host a script.
* @see [[DisplayStyleSettings.renderTimeline]] to associate a [RenderTimeline]($backend)'s script with a [DisplayStyle]($backend).
* @see [DisplayStyleState.scheduleScript]($frontend) to obtain the script associated with a display style.
* @see [[RenderSchedule.ScriptBuilder]] to define a new script.
*/
class Script {
/** Timelines specifying how to animate individual models within the view. */
modelTimelines;
/** True if this script applies clipping to any models. */
containsModelClipping;
/** If true, applying this timeline requires special tiles to be generated in which groups of elements are batched into nodes.
* @internal
*/
requiresBatching;
/** True if this script affects the position, orientation, or scale of the geometry. */
containsTransform;
/** True if this script affects the color or transparency of the geometry. */
containsFeatureOverrides;
/** The total time period over which this script animates. */
duration;
/** The batchIds of all nodes in all timelines that apply a transform.
* @internal
*/
transformBatchIds;
/** Tile tree references perform **very** frequent ordered comparisons of Scripts. They need to be fast. */
_cachedComparisons = new WeakMap();
_discreteBatchIds;
_lastFeatureModelTimeline;
_maxBatchId;
compareTo(other) {
if (this === other)
return 0;
const cached = this._cachedComparisons.get(other);
if (undefined !== cached)
return cached;
let cmp = compareNumbers(this.modelTimelines.length, other.modelTimelines.length) || compareBooleans(this.containsModelClipping, other.containsModelClipping)
|| compareBooleans(this.requiresBatching, other.requiresBatching) || compareBooleans(this.containsTransform, other.containsTransform)
|| compareBooleans(this.containsFeatureOverrides, other.containsFeatureOverrides) || compareDurations(this.duration, other.duration);
if (0 === cmp) {
for (let i = 0; i < this.modelTimelines.length; i++)
if (0 !== (cmp = this.modelTimelines[i].compareTo(other.modelTimelines[i])))
break;
}
this._cachedComparisons.set(other, cmp);
other._cachedComparisons.set(this, -cmp);
return cmp;
}
equals(other) {
return 0 === this.compareTo(other);
}
constructor(props) {
this.duration = Range1d.createNull();
const transformBatchIds = new Set();
const modelTimelines = [];
let containsModelClipping = false;
let requiresBatching = false;
let containsTransform = false;
let containsFeatureOverrides = false;
for (const modelProps of props) {
const model = ModelTimeline.fromJSON(modelProps);
modelTimelines.push(model);
this.duration.extendRange(model.duration);
containsModelClipping ||= model.containsModelClipping;
requiresBatching ||= model.requiresBatching;
containsTransform ||= model.containsTransform;
containsFeatureOverrides ||= model.containsFeatureOverrides;
for (const batchId of model.transformBatchIds)
transformBatchIds.add(batchId);
}
this.modelTimelines = modelTimelines;
this.containsModelClipping = containsModelClipping;
this.containsTransform = containsTransform;
this.requiresBatching = requiresBatching || this.containsTransform;
this.containsFeatureOverrides = containsFeatureOverrides;
this.transformBatchIds = transformBatchIds;
}
static fromJSON(props) {
if (!Array.isArray(props) || props.length === 0)
return undefined;
return new Script(props);
}
toJSON() {
return this.modelTimelines.map((x) => x.toJSON());
}
/** Look up the timeline that animates the specified model, if any. */
find(modelId) {
return this.modelTimelines.find((x) => x.modelId === modelId);
}
/** @internal */
getTransformBatchIds(modelId) {
return this.find(modelId)?.transformBatchIds;
}
/** @internal */
getTransform(modelId, batchId, time) {
return this.find(modelId)?.getTransform(batchId, time);
}
/** @internal */
addSymbologyOverrides(overrides, time) {
for (const timeline of this.modelTimelines)
timeline.addSymbologyOverrides(overrides, time);
}
/** Used by the [Entity.collectReferenceIds]($backend) method overrides in RenderTimeline and DisplayStyle.
* @internal
*/
discloseIds(ids) {
for (const model of this.modelTimelines) {
ids.addModel(model.modelId);
for (const element of model.elementTimelines)
for (const id of element.elementIds)
ids.addElement(id);
}
}
/** @internal */
modelRequiresBatching(modelId) {
// Only if the script contains animation (cutting plane, transform or visibility by node ID) do we require separate tilesets for animations.
return this.requiresBatching && this.modelTimelines.some((x) => x.modelId === modelId && x.requiresBatching);
}
/** The batch Ids of the subset of [[elementTimelines]] that apply a transform and/or cutting plane.
* @alpha
*/
get discreteBatchIds() {
if (this._discreteBatchIds)
return this._discreteBatchIds;
this._discreteBatchIds = new Set();
for (const timeline of this.modelTimelines)
for (const batchId of timeline.discreteBatchIds)
this._discreteBatchIds.add(batchId);
return this._discreteBatchIds;
}
/** @internal see ImdlReader.readAnimationBranches. */
getBatchIdForFeature(feature) {
let timeline;
const prev = this._lastFeatureModelTimeline;
if (prev && prev.idLower === feature.modelId.lower && prev.idUpper === feature.modelId.upper) {
timeline = prev.timeline;
}
else {
const modelId = Id64.fromUint32PairObject(feature.modelId);
timeline = this.find(modelId);
this._lastFeatureModelTimeline = { timeline, idLower: feature.modelId.lower, idUpper: feature.modelId.upper };
}
return timeline?.getBatchIdForFeature(feature) ?? 0;
}
/** @alpha */
get maxBatchId() {
return this._maxBatchId ?? (this._maxBatchId = this.modelTimelines.reduce((accum, timeline) => Math.max(accum, timeline.maxBatchId), 0));
}
/**
* Replaces all elementIds in a ScriptProps object with an empty string. Returns modified ScriptProps.
* @param scheduleScript The script props to modify.
* @internal */
static removeScheduleScriptElementIds(scheduleScript) {
scheduleScript.forEach((modelTimeline) => {
modelTimeline.elementTimelines.forEach((elementTimeline) => {
if (elementTimeline.elementIds) {
elementTimeline.elementIds = "";
}
});
});
return scheduleScript;
}
}
RenderSchedule.Script = Script;
/** A reference to a [[RenderSchedule.Script]], optionally identifying the source of the script.
* @see [DisplayStyle.loadScheduleScript]($backend) to obtain the script reference for a display style.
* @see [DisplayStyleState.scheduleScript]($frontend) or [DisplayStyleState.changeRenderTimeline]($frontend) to change a display style's script on the frontend.
*/
class ScriptReference {
/** The Id of the element - if any - from which the script originated.
* A schedule script may originate from one of the following sources:
* - A [RenderTimeline]($backend) element stored in the iModel; or
* - The `scheduleScript` JSON property of a [DisplayStyle]($backend) element stored in the iModel; or
* - Any other source outside of the iModel, such as code that generates the script on the frontend, a script obtained from some server, etc.
*
* The [[sourceId]] property identifies the Id of the element from which the script originated; an empty or invalid [Id64String]($bentley) indicates the script did not
* originate from any persistent element. If the Id is valid, the contents of [[script]] are assumed to match those stored on the source element.
*/
sourceId;
/** The script defining the rendering timelines to be applied. */
script;
/** @internal Use one of the public constructor overloads which forward to this one. */
constructor(sourceIdOrScript, scriptIfSourceId) {
if (typeof sourceIdOrScript === "string") {
assert(scriptIfSourceId instanceof Script);
this.sourceId = sourceIdOrScript;
this.script = scriptIfSourceId;
}
else {
assert(undefined === scriptIfSourceId);
this.script = sourceIdOrScript;
this.sourceId = Id64.invalid;
}
}
}
RenderSchedule.ScriptReference = ScriptReference;
/** Used as part of a [[RenderSchedule.ScriptBuilder]] to define a [[RenderSchedule.Timeline]]. Functions that append
* to the timeline expect entries to be appended in chronological order - i.e., you cannot append an entry that is earlier
* than a previously appended entry.
* @see [[RenderSchedule.ElementTimelineBuilder]] and [[RenderSchedule.ModelTimelineBuilder]].
*/
class TimelineBuilder {
/** Timeline controlling visibility. */
visibility;
/** Timeline controlling color. */
color;
/** Timeline controlling position and orientation. */
transform;
/** Timeline controlling clipping. */
cuttingPlane;
/** Append a new [[RenderSchedule.VisibilityEntry]] to the timeline. `time` must be more recent than any previously-appended visibility entries. */
addVisibility(time, visibility, interpolation = Interpolation.Linear) {
if (!this.visibility)
this.visibility = [];
this.visibility.push({ time, value: visibility, interpolation });
}
/** Append a new [[RenderSchedule.ColorEntry]] to the timeline. `time` must be more recent than any previously-appended color entries. */
addColor(time, color, interpolation = Interpolation.Linear) {
if (!this.color)
this.color = [];
const value = color instanceof RgbColor ? { red: color.r, green: color.g, blue: color.b } : color;
this.color.push({ time, value, interpolation });
}