@itwin/core-frontend
Version:
iTwin.js frontend components
350 lines • 17.5 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 WebGL
*/
import { assert, dispose, Id64 } from "@itwin/core-bentley";
import { PackedFeature } from "@itwin/core-common";
import { LineCode } from "./LineCode";
import { GL } from "./GL";
import { TextureUnit } from "./RenderFlags";
import { sync } from "./Sync";
import { System } from "./System";
import { Texture2DDataUpdater, TextureHandle } from "./Texture";
import { DisplayParams } from "../../../common/internal/render/DisplayParams";
import { computeDimensions } from "../../../common/internal/render/VertexTable";
export function isFeatureHilited(feature, hilites, isModelHilited) {
if (hilites.isEmpty)
return false;
if ("union" === hilites.modelSubCategoryMode)
return isModelHilited || hilites.elements.hasPair(feature.elementId) || hilites.subcategories.hasPair(feature.subCategoryId);
return hilites.elements.hasPair(feature.elementId) || (isModelHilited && hilites.subcategories.hasPair(feature.subCategoryId));
}
const scratchPackedFeature = PackedFeature.createWithIndex();
/** @internal */
export class FeatureOverrides {
target;
_options;
_lut;
_mostRecentSymbologyOverrides;
_lastFlashId = Id64.invalid;
_hiliteSyncObserver = {};
_pickExclusionsSyncObserver = {};
_anyOverridden = true;
_allHidden = true;
_anyTranslucent = true;
_anyViewIndependentTranslucent = true;
_anyOpaque = true;
_anyHilited = true;
_lutParams = new Float32Array(2);
_uniformSymbologyFlags = 0 /* EmphasisFlags.None */;
_cleanup;
get anyOverridden() { return this._anyOverridden; }
get allHidden() { return this._allHidden; }
get anyTranslucent() { return this._anyTranslucent; }
get anyViewIndependentTranslucent() { return this._anyViewIndependentTranslucent; }
get anyOpaque() { return this._anyOpaque; }
get anyHilited() { return this._anyHilited; }
/** For tests. */
get lutData() { return this._lut?.dataBytes; }
get byteLength() { return undefined !== this._lut ? this._lut.bytesUsed : 0; }
get isUniform() { return 3 === this._lutParams[0] && 1 === this._lutParams[1]; }
updateUniformSymbologyFlags() {
this._uniformSymbologyFlags = 0 /* EmphasisFlags.None */;
if (!this.isUniform || !this._lut)
return;
let flags = this._lut.dataBytes[0];
if (0 !== (flags & 16 /* OvrFlags.Flashed */))
this._uniformSymbologyFlags |= 4 /* EmphasisFlags.Flashed */;
if (0 !== (flags & 32 /* OvrFlags.NonLocatable */))
this._uniformSymbologyFlags |= 8 /* EmphasisFlags.NonLocatable */;
if (!this._anyHilited)
return;
flags = this._lut.dataBytes[1] << 8;
if (0 !== (flags & 256 /* OvrFlags.Hilited */))
this._uniformSymbologyFlags |= 1 /* EmphasisFlags.Hilite */;
if (0 !== (flags & 512 /* OvrFlags.Emphasized */))
this._uniformSymbologyFlags |= 2 /* EmphasisFlags.Emphasized */;
}
getUniformOverrides() {
assert(this.isUniform);
assert(undefined !== this._lut);
assert(undefined !== this._lut.dataBytes);
return this._lut.dataBytes;
}
_initialize(provider, map, ovrs, pickExcludes, hilite, flashed) {
const nFeatures = map.numFeatures;
const dims = computeDimensions(nFeatures, 3, 0, System.instance.maxTextureSize);
const width = dims.width;
const height = dims.height;
assert(width * height >= nFeatures);
this._lutParams[0] = width;
this._lutParams[1] = height;
const data = new Uint8Array(width * height * 4);
const creator = new Texture2DDataUpdater(data);
this.buildLookupTable(provider, creator, map, ovrs, pickExcludes, flashed, hilite);
return TextureHandle.createForData(width, height, data, true, GL.Texture.WrapMode.ClampToEdge);
}
_update(provider, map, lut, pickExcludes, flashed, hilites, ovrs) {
const updater = new Texture2DDataUpdater(lut.dataBytes);
if (undefined === ovrs) {
this.updateFlashedAndHilited(updater, map, pickExcludes, flashed, hilites);
}
else {
assert(undefined !== hilites);
this.buildLookupTable(provider, updater, map, ovrs, pickExcludes, flashed, hilites);
}
lut.update(updater);
}
setTransparency(transparency, viewDependentTransparency, data, transparencyByteIndex, curFlags) {
// transparency in range [0, 1]...convert to byte and invert so 0=transparent...
let alpha = 1.0 - transparency;
alpha = Math.floor(0xff * alpha + 0.5);
if ((0xff - alpha) < DisplayParams.minTransparency)
alpha = 0xff;
data.setByteAtIndex(transparencyByteIndex, alpha);
if (0xff === alpha) {
this._anyOpaque = true;
}
else {
this._anyTranslucent = true;
if (!viewDependentTransparency) {
curFlags |= 1024 /* OvrFlags.ViewIndependentTransparency */;
this._anyViewIndependentTranslucent = true;
}
}
return curFlags;
}
buildLookupTable(provider, data, map, ovr, pickExclude, flashedIdParts, hilites) {
const allowHilite = true !== this._options.noHilite;
const allowFlash = true !== this._options.noFlash;
const allowEmphasis = true !== this._options.noEmphasis;
let isModelHilited = false;
const prevModelId = { lower: -1, upper: -1 };
this._anyOpaque = this._anyTranslucent = this._anyViewIndependentTranslucent = this._anyHilited = false;
let nHidden = 0;
let nOverridden = 0;
// NB: We currently use 3 RGBA values per feature as follows:
// [0]
// RG = override flags (see OvrFlags enum)
// B = line code
// A = line weight (if we need an extra byte in future, could combine code+weight into a single byte).
// [1]
// RGB = rgb
// A = alpha
// [2]
// RGB = line rgb
// A = line alpha
for (const feature of map.iterable(scratchPackedFeature)) {
const i = feature.index;
const dataIndex = i * 4 * 3;
if (prevModelId.lower !== feature.modelId.lower || prevModelId.upper !== feature.modelId.upper) {
prevModelId.lower = feature.modelId.lower;
prevModelId.upper = feature.modelId.upper;
isModelHilited = allowHilite && hilites.models.hasPair(feature.modelId);
}
const app = provider.getFeatureAppearance(ovr, feature.elementId.lower, feature.elementId.upper, feature.subCategoryId.lower, feature.subCategoryId.upper, feature.geometryClass, feature.modelId.lower, feature.modelId.upper, map.type, feature.animationNodeId);
// NB: If the appearance is fully transparent, then:
// - For normal ("primary") models, getAppearance() returns undefined.
// - For classifier models, getAppearance() returns the appearance, and classification shader will discard fully-transparent classified pixels.
// (The latter is how we clip the classified model using the classifiers).
if (undefined === app) {
// The feature is not visible. We don't care about any of the other overrides, because we're not going to render it.
data.setOvrFlagsAtIndex(dataIndex, 4096 /* OvrFlags.Visibility */);
nHidden++;
nOverridden++;
continue;
}
let flags = app.nonLocatable ? 32 /* OvrFlags.NonLocatable */ : 0 /* OvrFlags.None */;
if (allowHilite && isFeatureHilited(feature, hilites, isModelHilited)) {
flags |= 256 /* OvrFlags.Hilited */;
this._anyHilited = true;
}
if (allowEmphasis && app.emphasized) {
flags |= 512 /* OvrFlags.Emphasized */;
this._anyHilited = true;
}
if (app.overridesRgb && app.rgb) {
flags |= 2 /* OvrFlags.Rgb */;
const rgb = app.rgb;
data.setByteAtIndex(dataIndex + 4, rgb.r);
data.setByteAtIndex(dataIndex + 5, rgb.g);
data.setByteAtIndex(dataIndex + 6, rgb.b);
}
if (undefined !== app.transparency) {
flags |= 4 /* OvrFlags.Alpha */;
flags = this.setTransparency(app.transparency, app.viewDependentTransparency, data, dataIndex + 7, flags);
}
const lineRgb = app.getLineRgb();
if (lineRgb) {
flags |= 1 /* OvrFlags.LineRgb */;
data.setByteAtIndex(dataIndex + 8, lineRgb.r);
data.setByteAtIndex(dataIndex + 9, lineRgb.g);
data.setByteAtIndex(dataIndex + 10, lineRgb.b);
}
const lineTransp = app.getLineTransparency();
if (undefined !== lineTransp) {
flags |= 8 /* OvrFlags.LineAlpha */;
flags = this.setTransparency(lineTransp, app.viewDependentTransparency, data, dataIndex + 11, flags);
}
if (app.overridesWeight && app.weight) {
flags |= 128 /* OvrFlags.Weight */;
let weight = app.weight;
weight = Math.min(31, weight);
weight = Math.max(1, weight);
data.setByteAtIndex(dataIndex + 3, weight);
}
if (app.overridesLinePixels && app.linePixels) {
flags |= 64 /* OvrFlags.LineCode */;
const lineCode = LineCode.valueFromLinePixels(app.linePixels);
data.setByteAtIndex(dataIndex + 2, lineCode);
}
if (app.ignoresMaterial)
flags |= 8192 /* OvrFlags.IgnoreMaterial */;
if (allowFlash && undefined !== flashedIdParts && feature.elementId.lower === flashedIdParts.lower && feature.elementId.upper === flashedIdParts.upper)
flags |= 16 /* OvrFlags.Flashed */;
if (pickExclude?.hasPair(feature.elementId)) {
flags |= 2048 /* OvrFlags.InvisibleDuringPick */;
++nHidden;
}
data.setOvrFlagsAtIndex(dataIndex, flags);
if (0 /* OvrFlags.None */ !== flags)
nOverridden++;
}
this._allHidden = (nHidden === map.numFeatures);
this._anyOverridden = (nOverridden > 0);
this.updateUniformSymbologyFlags();
}
// NB: If hilites is undefined, it means that the hilited set has not changed.
updateFlashedAndHilited(data, map, pickExcludes, flashed, hilites) {
if (!hilites || true === this._options.noHilite) {
this.updateFlashed(data, map, pickExcludes, flashed);
return;
}
const allowFlash = true !== this._options.noFlash;
const intersect = "intersection" === hilites.modelSubCategoryMode;
this._anyOverridden = this._anyHilited = false;
for (const feature of map.iterable(scratchPackedFeature)) {
const dataIndex = feature.index * 4 * 3;
const oldFlags = data.getOvrFlagsAtIndex(dataIndex);
if (0 /* OvrFlags.None */ !== (oldFlags & 4096 /* OvrFlags.Visibility */)) {
// If it's invisible, none of the other flags matter. We can't flash it and don't want to hilite it.
this._anyOverridden = true;
continue;
}
const isModelHilited = hilites.models.hasPair(feature.modelId);
let isHilited = isModelHilited && !intersect;
if (!isHilited)
isHilited = hilites.elements.hasPair(feature.elementId);
if (!isHilited)
if (isModelHilited || !intersect)
isHilited = hilites.subcategories.hasPair(feature.subCategoryId);
let isFlashed = false;
if (flashed && allowFlash)
isFlashed = feature.elementId.lower === flashed.lower && feature.elementId.upper === flashed.upper;
let newFlags = isFlashed ? (oldFlags | 16 /* OvrFlags.Flashed */) : (oldFlags & ~16 /* OvrFlags.Flashed */);
newFlags = isHilited ? (newFlags | 256 /* OvrFlags.Hilited */) : (newFlags & ~256 /* OvrFlags.Hilited */);
if (pickExcludes) {
newFlags = pickExcludes.hasPair(feature.elementId) ? (newFlags | 2048 /* OvrFlags.InvisibleDuringPick */) : (newFlags & ~2048 /* OvrFlags.InvisibleDuringPick */);
}
data.setOvrFlagsAtIndex(dataIndex, newFlags);
if (0 /* OvrFlags.None */ !== newFlags) {
this._anyOverridden = true;
this._anyHilited = this._anyHilited || isHilited || 0 /* OvrFlags.None */ !== (newFlags & 512 /* OvrFlags.Emphasized */);
}
}
this.updateUniformSymbologyFlags();
}
updateFlashed(data, map, pickExcludes, flashed) {
if (true === this._options.noFlash && !pickExcludes)
return;
this._anyOverridden = false;
const elemId = { lower: 0, upper: 0 };
for (let i = 0; i < map.numFeatures; i++) {
const dataIndex = i * 4 * 3;
const oldFlags = data.getOvrFlagsAtIndex(dataIndex);
if (0 /* OvrFlags.None */ !== (oldFlags & 4096 /* OvrFlags.Visibility */)) {
// If it's invisible, none of the other flags matter and we can't flash it.
this._anyOverridden = true;
continue;
}
let isFlashed = false;
let thisElemId;
if (flashed && !this._options.noFlash) {
thisElemId = map.getElementIdPair(i, elemId);
isFlashed = thisElemId.lower === flashed.lower && thisElemId.upper === flashed.upper;
}
let newFlags = isFlashed ? (oldFlags | 16 /* OvrFlags.Flashed */) : (oldFlags & ~16 /* OvrFlags.Flashed */);
if (pickExcludes) {
if (!thisElemId) {
thisElemId = map.getElementIdPair(i, elemId);
}
newFlags = pickExcludes.hasPair(thisElemId) ? (newFlags | 2048 /* OvrFlags.InvisibleDuringPick */) : (newFlags & ~2048 /* OvrFlags.InvisibleDuringPick */);
}
data.setOvrFlagsAtIndex(dataIndex, newFlags);
if (0 /* OvrFlags.None */ !== newFlags)
this._anyOverridden = true;
}
this.updateUniformSymbologyFlags();
}
constructor(target, options, cleanup) {
this.target = target;
this._options = options;
this._cleanup = cleanup;
}
static createFromTarget(target, options, cleanup) {
return new FeatureOverrides(target, options, cleanup);
}
get isDisposed() { return undefined === this._lut; }
[Symbol.dispose]() {
this._lut = dispose(this._lut);
if (this._cleanup) {
this._cleanup();
this._cleanup = undefined;
}
}
initFromMap(map, provider) {
const nFeatures = map.numFeatures;
assert(0 < nFeatures);
this._lut = dispose(this._lut);
const ovrs = this.target.currentFeatureSymbologyOverrides;
this._mostRecentSymbologyOverrides = ovrs;
const hilite = this.target.hilites;
this._lut = this._initialize(provider, map, ovrs, this.target.pickExclusions, hilite, this.target.flashed);
this._lastFlashId = Id64.invalid;
this._hiliteSyncObserver = {};
this._pickExclusionsSyncObserver = {};
}
update(features, provider) {
let ovrs = this.target.currentFeatureSymbologyOverrides;
const ovrsUpdated = ovrs !== this._mostRecentSymbologyOverrides;
if (ovrsUpdated)
this._mostRecentSymbologyOverrides = ovrs;
else
ovrs = undefined;
const flashedId = this.target.flashedId;
const hilite = this.target.hilites;
const hiliteUpdated = !sync(this.target.hiliteSyncTarget, this._hiliteSyncObserver);
const pickExcludesUpdated = !sync(this.target.pickExclusionsSyncTarget, this._pickExclusionsSyncObserver);
if (ovrsUpdated || hiliteUpdated || flashedId !== this._lastFlashId || pickExcludesUpdated) {
// _lut can be undefined if context was lost, (gl.createTexture returns null)
if (this._lut) {
this._update(provider, features, this._lut, undefined !== ovrs || pickExcludesUpdated ? this.target.pickExclusions : undefined, this.target.flashed, undefined !== ovrs || hiliteUpdated ? hilite : undefined, ovrs);
}
this._lastFlashId = flashedId;
}
}
bindLUTParams(uniform) {
uniform.setUniform2fv(this._lutParams);
}
bindLUT(uniform) {
if (this._lut)
this._lut.bindSampler(uniform, TextureUnit.FeatureSymbology);
}
bindUniformSymbologyFlags(uniform) {
uniform.setUniform1f(this._uniformSymbologyFlags);
}
}
//# sourceMappingURL=FeatureOverrides.js.map