@itwin/core-common
Version:
iTwin.js components common to frontend and backend
335 lines • 16.1 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, JsonUtils } from "@itwin/core-bentley";
import { Vector3d } from "@itwin/core-geometry";
import { RgbColor } from "./RgbColor";
function extractIntensity(value, defaultValue) {
const maxIntensity = 5;
return typeof value === "number" ? Math.max(0, Math.min(maxIntensity, value)) : defaultValue;
}
const defaultSolarDirection = Vector3d.create(0.272166, 0.680414, 0.680414);
/** Describes the solar directional light associated with a [[LightSettings]].
* @see [[SolarLightProps]].
* @public
*/
export class SolarLight {
/** Direction of the light in world coordinates. Defaults to a vector looking down on the scene at a 45 degree angle mostly along the Y axis. */
direction;
/** Intensity of the light, typically in [0..1] but can range up to 5. Default: 1.0 */
intensity;
/** If true, the light will be applied even when shadows are turned off for the view.
* If false, a roughly overhead light of the same intensity oriented in view space will be used instead.
* Default: false.
*/
alwaysEnabled;
/** If defined, the time in UNIX milliseconds from which [[direction]] was calculated.
* @see [[DisplayStyleSettings.setSunTime]] to compute the solar direction from a point in time.
*/
timePoint;
constructor(json) {
json = json || {};
this.intensity = extractIntensity(json.intensity, 1);
this.alwaysEnabled = JsonUtils.asBool(json.alwaysEnabled);
if (json.direction)
this.direction = Vector3d.fromJSON(json.direction);
else
this.direction = defaultSolarDirection.clone();
if (typeof json.timePoint === "number")
this.timePoint = json.timePoint;
}
toJSON() {
const direction = this.direction.isAlmostEqual(defaultSolarDirection) ? undefined : this.direction.toJSON();
const intensity = this.intensity !== 1 ? this.intensity : undefined;
const alwaysEnabled = this.alwaysEnabled ? true : undefined;
const timePoint = this.timePoint;
if (undefined === direction && undefined === intensity && undefined === alwaysEnabled && undefined === timePoint)
return undefined;
const json = {};
if (direction)
json.direction = direction;
if (undefined !== intensity)
json.intensity = intensity;
if (undefined !== alwaysEnabled)
json.alwaysEnabled = alwaysEnabled;
if (undefined !== timePoint)
json.timePoint = timePoint;
return json;
}
/** Create a copy of this SolarLight, identical except in any properties explicitly specified by `changedProps`, with a possible exception for [[timePoint]].
* If `this.timePoint` is defined and `changedProps` defines `direction` but **doesn't** define `timePoint`, the time point will only be preserved in the
* copy if `changesProps.direction` is equal to `this.direction`.
*/
clone(changedProps) {
if (!changedProps)
return this;
const props = this.toJSON() ?? {};
if (undefined !== changedProps.direction)
props.direction = changedProps.direction;
if (undefined !== changedProps.intensity)
props.intensity = changedProps.intensity;
if (undefined !== changedProps.alwaysEnabled)
props.alwaysEnabled = changedProps.alwaysEnabled;
if (undefined !== changedProps.timePoint)
props.timePoint = changedProps.timePoint;
// If our direction was computed from a time point and the caller only supplies a direction, invalidate the time point unless the input direction matches our direction.
// If caller explicitly supplied a timePoint, trust it.
if (undefined !== this.timePoint && undefined === changedProps.timePoint && undefined !== changedProps.direction) {
const newDirection = Vector3d.fromJSON(changedProps.direction);
if (!newDirection.isAlmostEqual(this.direction))
props.timePoint = undefined;
}
return new SolarLight(props);
}
equals(rhs) {
return this.intensity === rhs.intensity && this.alwaysEnabled === rhs.alwaysEnabled && this.direction.isExactEqual(rhs.direction) && this.timePoint === rhs.timePoint;
}
}
/** Describes the ambient light associated with a [[LightSettings]].
* @see [[AmbientLightProps]]
* @public
*/
export class AmbientLight {
color;
intensity;
constructor(json) {
json = json || {};
this.intensity = extractIntensity(json.intensity, 0.2);
this.color = json.color ? RgbColor.fromJSON(json.color) : new RgbColor(0, 0, 0);
}
toJSON() {
const color = this.color.r !== 0 || this.color.g !== 0 || this.color.b !== 0 ? this.color.toJSON() : undefined;
const intensity = 0.2 !== this.intensity ? this.intensity : undefined;
if (undefined === color && undefined === intensity)
return undefined;
const json = {};
if (color)
json.color = color;
if (undefined !== intensity)
json.intensity = intensity;
return json;
}
/** Create a copy of this light, identical except for any properties explicitly specified by `changed`. */
clone(changed) {
if (!changed)
return this;
const props = this.toJSON() ?? {};
if (undefined !== changed.intensity)
props.intensity = changed.intensity;
if (undefined !== changed.color)
props.color = changed.color;
return new AmbientLight(props);
}
equals(rhs) {
return this.intensity === rhs.intensity && this.color.equals(rhs.color);
}
}
const defaultUpperHemisphereColor = new RgbColor(143, 205, 255);
const defaultLowerHemisphereColor = new RgbColor(120, 143, 125);
/** Describes a pair of hemisphere lights associated with a [[LightSettings]].
* @see [[HemisphereLightsProps]]
* @public
*/
export class HemisphereLights {
upperColor;
lowerColor;
intensity;
constructor(json) {
json = json || {};
this.intensity = extractIntensity(json.intensity, 0);
this.upperColor = json.upperColor ? RgbColor.fromJSON(json.upperColor) : defaultUpperHemisphereColor;
this.lowerColor = json.lowerColor ? RgbColor.fromJSON(json.lowerColor) : defaultLowerHemisphereColor;
}
toJSON() {
const upperColor = this.upperColor.equals(defaultUpperHemisphereColor) ? undefined : this.upperColor.toJSON();
const lowerColor = this.lowerColor.equals(defaultLowerHemisphereColor) ? undefined : this.lowerColor.toJSON();
const intensity = 0 === this.intensity ? undefined : this.intensity;
if (undefined === upperColor && undefined === lowerColor && undefined === intensity)
return undefined;
const json = {};
if (upperColor)
json.upperColor = upperColor;
if (lowerColor)
json.lowerColor = lowerColor;
if (undefined !== intensity)
json.intensity = intensity;
return json;
}
/** Create a copy of these lights, identical except for any properties explicitly specified by `changed`. */
clone(changed) {
if (!changed)
return this;
const props = this.toJSON() || {};
if (undefined !== changed.upperColor)
props.upperColor = changed.upperColor;
if (undefined !== changed.lowerColor)
props.lowerColor = changed.lowerColor;
if (undefined !== changed.intensity)
props.intensity = changed.intensity;
return new HemisphereLights(props);
}
equals(rhs) {
return this.intensity === rhs.intensity && this.upperColor.equals(rhs.upperColor) && this.lowerColor.equals(rhs.lowerColor);
}
}
function clampIntensity(intensity = 0) {
return Math.max(intensity, 0);
}
/** As part of a [[LightSettings]], describes how to apply a Fresnel effect to the contents of the view.
* The "Fresnel effect" is based on the observation that the reflectivity of a surface varies based on the angle between the surface and
* the viewer's line of sight. For example, a flat surface will appear more reflective when viewed at a glancing angle than it will when
* viewed from above; and a sphere will appear more reflective around its edges than at its center.
*
* This principle can be used to improve photorealism, but the implementation provided here is intended to produce non-realistic but
* aesthetically-pleasing results.
* @see [[LightSettings.fresnel]].
* @public
*/
export class FresnelSettings {
/** The strength of the effect in terms of how much brighter the surface becomes. The intensity at a given point on the surface is determined by
* the angle between the viewer's line of sight and the vector from the viewer to that point. Maximum intensity is produced when those vectors are
* perpendicular, and zero intensity is produced when those vectors are parallel (unless [[invert]] is `true`).
*
* A value of zero turns off the effect. Values less than zero are clamped to zero. Typical values range between 0 and 1.
*/
intensity;
/** If true, inverts the effect's [[intensity]] such that maximum intensity is produced when the viewer's line of sight is parallel to the vector between
* the viewer and the point on the surface, and zero intensity is produced when the viewer's line of sight is perpendicular to that vector.
*/
invert;
constructor(intensity, invert) {
assert(intensity >= 0);
this.intensity = intensity;
this.invert = invert;
}
static _defaults = new FresnelSettings(0, false);
/** Create from JSON representation, using default values for any unspecified or `undefined` properties. */
static fromJSON(props) {
const intensity = clampIntensity(JsonUtils.asDouble(props?.intensity));
const invert = JsonUtils.asBool(props?.invert);
if (0 === intensity && !invert)
return this._defaults;
return new this(intensity, invert);
}
/** Create a new FresnelSettings.
* @note Intensity values less than zero will be set to zero.
*/
static create(intensity = 0, invert = false) {
return this.fromJSON({ intensity, invert });
}
/** Convert to JSON representation.
* @note If all settings match the default values, `undefined` will be returned.
*/
toJSON() {
if (0 === this.intensity && !this.invert)
return undefined;
const props = {};
if (0 !== this.intensity)
props.intensity = this.intensity;
if (this.invert)
props.invert = true;
return props;
}
/** Create a copy of these settings, modified to match any properties explicitly specified by `changedProps`. */
clone(changedProps) {
if ((undefined === changedProps?.intensity || changedProps.intensity === this.intensity)
&& (undefined === changedProps?.invert || changedProps.invert === this.invert))
return this;
const intensity = changedProps?.intensity ?? this.intensity;
const invert = changedProps?.invert ?? this.invert;
return FresnelSettings.fromJSON({ intensity, invert });
}
/** Return true if these settings are equivalent to `rhs`. */
equals(rhs) {
return this === rhs || (this.intensity === rhs.intensity && this.invert === rhs.invert);
}
}
/** Describes the lighting for a 3d scene, associated with a [[DisplayStyle3dSettings]] in turn associated with a [DisplayStyle3d]($backend) or [DisplayStyle3dState]($frontend).
* @see [[LightSettingsProps]]
* @public
*/
export class LightSettings {
solar;
ambient;
hemisphere;
/** See [[LightSettingsProps.portrait]]. */
portraitIntensity;
specularIntensity;
/** See [[LightSettingsProps.numCels]]. */
numCels;
fresnel;
constructor(solar, ambient, hemisphere, portraitIntensity, specularIntensity, numCels, fresnel) {
this.solar = solar;
this.ambient = ambient;
this.hemisphere = hemisphere;
this.portraitIntensity = portraitIntensity;
this.specularIntensity = specularIntensity;
this.numCels = numCels;
this.fresnel = fresnel;
}
static fromJSON(props) {
const solar = new SolarLight(props?.solar);
const ambient = new AmbientLight(props?.ambient);
const hemisphere = new HemisphereLights(props?.hemisphere);
const portraitIntensity = extractIntensity(props?.portrait?.intensity, 0.3);
const specularIntensity = extractIntensity(props?.specularIntensity, 1.0);
const numCels = JsonUtils.asInt(props?.numCels, 0);
const fresnel = FresnelSettings.fromJSON(props?.fresnel);
return new LightSettings(solar, ambient, hemisphere, portraitIntensity, specularIntensity, numCels, fresnel);
}
toJSON() {
const solar = this.solar.toJSON();
const ambient = this.ambient.toJSON();
const hemisphere = this.hemisphere.toJSON();
const portrait = 0.3 !== this.portraitIntensity ? { intensity: this.portraitIntensity } : undefined;
const specularIntensity = 1 !== this.specularIntensity ? this.specularIntensity : undefined;
const numCels = 0 !== this.numCels ? this.numCels : undefined;
const fresnel = this.fresnel.toJSON();
if (!solar && !ambient && !hemisphere && !portrait && undefined === specularIntensity && undefined === numCels && undefined === fresnel)
return undefined;
const json = {};
if (solar)
json.solar = solar;
if (ambient)
json.ambient = ambient;
if (hemisphere)
json.hemisphere = hemisphere;
if (portrait)
json.portrait = portrait;
if (undefined !== specularIntensity)
json.specularIntensity = specularIntensity;
if (undefined !== numCels)
json.numCels = numCels;
if (fresnel)
json.fresnel = fresnel;
return json;
}
/** Create a copy of these light settings, identical except for any properties explicitly specified by `changed`.
* Note that the solar, ambient, and hemisphere lights will also be cloned using their own `clone` methods - so for example, the following:
* ` clone({ ambient: { intensity: 0.5 } })`
* will overwrite the ambient light's intensity but preserve its current color, rather than replacing the color with the default color.
*/
clone(changed) {
if (!changed)
return this;
const solar = this.solar.clone(changed.solar);
const ambient = this.ambient.clone(changed.ambient);
const hemisphere = this.hemisphere.clone(changed.hemisphere);
const portrait = changed.portrait?.intensity ?? this.portraitIntensity;
const specular = changed.specularIntensity ?? this.specularIntensity;
const numCels = changed.numCels ?? this.numCels;
const fresnel = this.fresnel.clone(changed.fresnel);
return new LightSettings(solar, ambient, hemisphere, portrait, specular, numCels, fresnel);
}
equals(rhs) {
if (this === rhs)
return true;
return this.portraitIntensity === rhs.portraitIntensity && this.specularIntensity === rhs.specularIntensity && this.numCels === rhs.numCels
&& this.ambient.equals(rhs.ambient) && this.solar.equals(rhs.solar) && this.hemisphere.equals(rhs.hemisphere) && this.fresnel.equals(rhs.fresnel);
}
}
//# sourceMappingURL=LightSettings.js.map