UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

335 lines • 16.1 kB
/*--------------------------------------------------------------------------------------------- * 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