@itwin/core-frontend
Version:
iTwin.js frontend components
239 lines • 9.54 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 } from "@itwin/core-bentley";
import { ClipPlaneContainment, Point3d, Transform } from "@itwin/core-geometry";
import { IModelApp } from "../../../IModelApp";
import { FloatRgba } from "./FloatRGBA";
import { Texture2DHandle } from "./Texture";
import { ClipVolume } from "./ClipVolume";
import { GL } from "./GL";
const emptyClipData = new Uint8Array(0);
const emptyClip = {
numRows: 0,
getData: () => emptyClipData,
};
const scratchRangeCorners = [
new Point3d(), new Point3d(), new Point3d(), new Point3d(),
new Point3d(), new Point3d(), new Point3d(), new Point3d(),
];
function getRangeCorners(r) {
return r.corners(scratchRangeCorners);
}
/** Maintains a stack of ClipVolumes. The volumes nest such that the stack represents the intersection of all the volumes.
* The bottom of the stack represents the view's clip volume and is always present even if the view has no clip.
* It also maintains the inside/outside clip colors, where the alpha component is 1 if the color should be applied and 0 if not.
* @internal
*/
export class ClipStack {
/** Encoded data for all clips on the stack, update when the stack or the transform changes. */
_cpuBuffer;
/** A view of the encoded buffer in the format expected by the GPU. */
_gpuBuffer;
_texture;
/** The maximum number of rows we have ever required. Determines the texture height. Grows as needed, reallocating a larger texture, but never shrinks. */
_numTotalRows;
/** The number of rows in the texture actually required to encode the current contents of the stack. */
_numRowsInUse;
/** The first entry always represents the view clip. The rest are pushed and popped with GraphicBranches. */
_stack = [emptyClip];
/** True if we need to recompute the texture. */
_isStackDirty = false;
/** Obtain the transform to be applied to the clips - i.e., the view matrix. */
_getTransform;
/** If this returns false, the clip at the bottom of the stack is ignored. */
_wantViewClip;
/** If alpha is 1 then geometry inside the clip will be drawn in this color. */
_insideColor = FloatRgba.from(0, 0, 0, 0);
/** If alpha is 1 then geometry outside the clip will be drawn in this color. */
_outsideColor = FloatRgba.from(0, 0, 0, 0);
/** For detecting whether the transform changed from one invocation of setViewClip to the next. */
_prevTransform = Transform.createZero();
/** True if we want to colorize geometry intersecting clip planes */
_colorizeIntersection = false;
/** The style to colorize the geometry intersecting clip planes */
_intersectionStyle = FloatRgba.from(0, 0, 0, 0);
constructor(getTransform, wantViewClip) {
this._getTransform = getTransform;
this._wantViewClip = wantViewClip;
this._numTotalRows = this._numRowsInUse = 0;
this._cpuBuffer = new Uint8Array(this._numTotalRows);
this._gpuBuffer = this.allocateGpuBuffer();
}
get insideColor() {
return this._insideColor;
}
get outsideColor() {
return this._outsideColor;
}
get hasOutsideColor() {
return this.outsideColor.alpha !== 0;
}
get colorizeIntersection() {
return this._colorizeIntersection;
}
set colorizeIntersection(b) {
this._colorizeIntersection = b;
}
get intersectionStyle() {
return this._intersectionStyle;
}
get bytesUsed() {
return this._texture ? this._texture.bytesUsed : 0;
}
setViewClip(clip, style) {
assert(this._stack.length === 1);
this.updateColor(style.insideColor, this._insideColor);
this.updateColor(style.outsideColor, this._outsideColor);
this.updateIntersectionStyle(style.colorizeIntersection, style.intersectionStyle, this._intersectionStyle);
const transform = this._getTransform();
if (!transform.isAlmostEqual(this._prevTransform)) {
transform.clone(this._prevTransform);
this._isStackDirty = true;
}
const cur = this._stack[0];
if (cur === emptyClip) {
if (!clip)
return; // no change.
}
else if (!clip) {
this._stack[0] = emptyClip;
this._numRowsInUse = 0;
this._isStackDirty = true;
return;
}
else {
assert(cur instanceof ClipVolume);
if (cur.clipVector === clip) {
// We assume that the active view's ClipVector is never mutated in place, so if we are given the same ClipVector, we expect our RenderClipVolume to match it.
return;
}
}
// ClipVector has changed.
const newClip = IModelApp.renderSystem.createClipVolume(clip);
if (!newClip) {
this._isStackDirty = this._stack[0] !== emptyClip;
this._stack[0] = emptyClip;
this._numRowsInUse = 0;
}
else {
this.pop();
this.push(newClip);
}
}
push(clip) {
assert(clip instanceof ClipVolume);
this._stack.push(clip);
this._numRowsInUse += clip.numRows;
this._numTotalRows = Math.max(this._numRowsInUse, this._numTotalRows);
this._isStackDirty = true;
}
pop() {
assert(this._stack.length > 0);
const clip = this._stack.pop();
this._numRowsInUse -= (clip ? clip.numRows : 0);
}
get hasClip() {
return this.startIndex < this.endIndex;
}
get hasViewClip() {
return emptyClip !== this._stack[0] && this._wantViewClip();
}
get startIndex() {
assert(this._stack.length > 0);
return this._wantViewClip() ? 0 : this._stack[0].numRows;
}
get endIndex() {
return this._numRowsInUse;
}
get textureHeight() {
return this._numTotalRows;
}
get texture() {
this.updateTexture();
return this._texture;
}
isRangeClipped(range, transform) {
if (this.hasOutsideColor || !this.hasClip)
return false;
range = transform.multiplyRange(range, range);
const corners = getRangeCorners(range);
const startIndex = this._wantViewClip() && emptyClip !== this._stack[0] ? 0 : 1;
for (let i = startIndex; i < this._stack.length; i++) {
const clip = this._stack[i];
assert(clip instanceof ClipVolume);
if (ClipPlaneContainment.StronglyOutside === clip.clipVector.classifyPointContainment(corners))
return true;
}
return false;
}
/** Exposed strictly for tests. */
get clips() {
return this._stack;
}
/** Exposed strictly for tests. */
static get emptyViewClip() {
return emptyClip;
}
updateTexture() {
if (this._numTotalRows > 0 && (!this._texture || this._texture.height < this._numTotalRows)) {
// We need to resize the texture.
assert(this._isStackDirty);
this._isStackDirty = true;
this._texture = dispose(this._texture);
this._cpuBuffer = new Uint8Array(this._numTotalRows * 4 * 4);
this._gpuBuffer = this.allocateGpuBuffer();
}
if (this._isStackDirty) {
this._isStackDirty = false;
this.recomputeTexture();
}
}
recomputeTexture() {
// Copy each clip's data to the buffer, recording whether the buffer's contents actually changed.
let bufferDirty = false;
const transform = this._getTransform();
let bufferIndex = 0;
for (const clip of this._stack) {
const data = clip.getData(transform);
for (let i = 0; i < data.byteLength; i++) {
const byte = data[i];
bufferDirty = bufferDirty || byte !== this._cpuBuffer[bufferIndex];
this._cpuBuffer[bufferIndex++] = byte;
}
}
// If the contents have changed, upload the new texture data to the GPU.
if (bufferDirty) {
this.uploadTexture();
}
}
uploadTexture() {
if (this._texture)
this._texture.replaceTextureData(this._gpuBuffer);
else
this._texture = Texture2DHandle.createForData(1, this._numTotalRows, this._gpuBuffer, false, GL.Texture.WrapMode.ClampToEdge, GL.Texture.Format.Rgba);
assert(this._texture.height === this._numTotalRows);
}
allocateGpuBuffer() {
return new Float32Array(this._cpuBuffer.buffer);
}
updateColor(rgb, rgba) {
rgba.alpha = undefined !== rgb ? 1 : 0;
if (rgb)
rgba.setRgbColor(rgb);
}
updateIntersectionStyle(colorizeIntersection, style, _thisStyle) {
this._colorizeIntersection = colorizeIntersection === true ? true : false;
if (style !== undefined) {
if (style.color !== undefined)
_thisStyle.setRgbColor(style.color);
if (style.width !== undefined)
_thisStyle.alpha = style.width;
}
}
}
//# sourceMappingURL=ClipStack.js.map