| /* |
| * Copyright (C) 2014 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as Common from '../common/common.js'; |
| import * as i18n from '../i18n/i18n.js'; |
| import * as Platform from '../platform/platform.js'; |
| import type * as SDK from '../sdk/sdk.js'; |
| import * as UI from '../ui/ui.js'; |
| |
| import type {LayerView, LayerViewHost} from './LayerViewHost.js'; |
| import {LayerSelection, Selection, SnapshotSelection, Type, ScrollRectSelection} from './LayerViewHost.js'; |
| import {Events as TransformControllerEvents, TransformController} from './TransformController.js'; |
| |
| export const UIStrings = { |
| /** |
| *@description Text of a DOM element in DView of the Layers panel |
| */ |
| layerInformationIsNotYet: 'Layer information is not yet available.', |
| /** |
| *@description Accessibility label for canvas view in Layers tool |
| */ |
| dLayersView: '3D Layers View', |
| /** |
| *@description Text in DView of the Layers panel |
| */ |
| cantDisplayLayers: 'Can\'t display layers,', |
| /** |
| *@description Text in DView of the Layers panel |
| */ |
| webglSupportIsDisabledInYour: 'WebGL support is disabled in your browser.', |
| /** |
| *@description Text in DView of the Layers panel |
| *@example {about:gpu} PH1 |
| */ |
| checkSForPossibleReasons: 'Check {PH1} for possible reasons.', |
| /** |
| *@description Text for a checkbox in the toolbar of the Layers panel to show the area of slow scroll rect |
| */ |
| slowScrollRects: 'Slow scroll rects', |
| /** |
| * @description Text for a checkbox in the toolbar of the Layers panel. This is a noun, for a |
| * setting meaning 'display paints in the layers viewer'. 'Paints' here means 'paint events' i.e. |
| * when the browser draws pixels to the screen. |
| */ |
| paints: 'Paints', |
| /** |
| *@description A context menu item in the DView of the Layers panel |
| */ |
| resetView: 'Reset View', |
| /** |
| *@description A context menu item in the DView of the Layers panel |
| */ |
| showPaintProfiler: 'Show Paint Profiler', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('layer_viewer/Layers3DView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| const vertexPositionAttributes = new Map<WebGLProgram, number>(); |
| |
| const vertexColorAttributes = new Map<WebGLProgram, number>(); |
| |
| const textureCoordAttributes = new Map<WebGLProgram, number>(); |
| |
| const uniformMatrixLocations = new Map<WebGLProgram, WebGLUniformLocation|null>(); |
| |
| const uniformSamplerLocations = new Map<WebGLProgram, WebGLUniformLocation|null>(); |
| |
| const imageForTexture = new Map<WebGLTexture, HTMLImageElement>(); |
| |
| export class Layers3DView extends UI.Widget.VBox implements LayerView { |
| _failBanner: UI.Widget.VBox; |
| _layerViewHost: LayerViewHost; |
| _transformController: TransformController; |
| _canvasElement: HTMLCanvasElement; |
| _lastSelection: {[x: string]: Selection|null}; |
| _layerTree: SDK.LayerTreeBase.LayerTreeBase|null; |
| _textureManager: LayerTextureManager; |
| _chromeTextures: (WebGLTexture|undefined)[]; |
| _rects: Rectangle[]; |
| _snapshotLayers: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>; |
| _shaderProgram!: WebGLProgram|null; |
| _oldTextureScale!: number|undefined; |
| _depthByLayerId!: Map<string, number>; |
| _visibleLayers!: Set<SDK.LayerTreeBase.Layer>; |
| _maxDepth!: number; |
| _scale!: number; |
| _layerTexture?: {layer: SDK.LayerTreeBase.Layer, texture: WebGLTexture}|null; |
| _projectionMatrix?: DOMMatrix; |
| _whiteTexture?: WebGLTexture|null; |
| _gl?: WebGLRenderingContext|null; |
| _dimensionsForAutoscale?: {width: number, height: number}; |
| _needsUpdate?: boolean; |
| _panelToolbar?: UI.Toolbar.Toolbar; |
| _showSlowScrollRectsSetting?: Common.Settings.Setting<boolean>; |
| _showPaintsSetting?: Common.Settings.Setting<boolean>; |
| _mouseDownX?: number; |
| _mouseDownY?: number; |
| |
| constructor(layerViewHost: LayerViewHost) { |
| super(true); |
| |
| this.registerRequiredCSS('layer_viewer/layers3DView.css', {enableLegacyPatching: true}); |
| this.contentElement.classList.add('layers-3d-view'); |
| this._failBanner = new UI.Widget.VBox(); |
| this._failBanner.element.classList.add('full-widget-dimmed-banner'); |
| UI.UIUtils.createTextChild(this._failBanner.element, i18nString(UIStrings.layerInformationIsNotYet)); |
| |
| this._layerViewHost = layerViewHost; |
| this._layerViewHost.registerView(this); |
| this._transformController = new TransformController(this.contentElement as HTMLElement); |
| this._transformController.addEventListener(TransformControllerEvents.TransformChanged, this._update, this); |
| this._initToolbar(); |
| this._canvasElement = this.contentElement.createChild('canvas') as HTMLCanvasElement; |
| this._canvasElement.tabIndex = 0; |
| this._canvasElement.addEventListener('dblclick', this._onDoubleClick.bind(this), false); |
| this._canvasElement.addEventListener('mousedown', this._onMouseDown.bind(this), false); |
| this._canvasElement.addEventListener('mouseup', this._onMouseUp.bind(this), false); |
| this._canvasElement.addEventListener('mouseleave', this._onMouseMove.bind(this), false); |
| this._canvasElement.addEventListener('mousemove', this._onMouseMove.bind(this), false); |
| this._canvasElement.addEventListener('contextmenu', this._onContextMenu.bind(this), false); |
| UI.ARIAUtils.setAccessibleName(this._canvasElement, i18nString(UIStrings.dLayersView)); |
| |
| this._lastSelection = {}; |
| this._layerTree = null; |
| |
| this._textureManager = new LayerTextureManager(this._update.bind(this)); |
| |
| this._chromeTextures = []; |
| |
| this._rects = []; |
| |
| this._snapshotLayers = new Map(); |
| this._layerViewHost.setLayerSnapshotMap(this._snapshotLayers); |
| |
| this._layerViewHost.showInternalLayersSetting().addChangeListener(this._update, this); |
| } |
| |
| setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void { |
| this._layerTree = layerTree; |
| this._layerTexture = null; |
| delete this._oldTextureScale; |
| if (this._showPaints()) { |
| this._textureManager.setLayerTree(layerTree); |
| } |
| this._update(); |
| } |
| |
| showImageForLayer(layer: SDK.LayerTreeBase.Layer, imageURL?: string): void { |
| if (!imageURL) { |
| this._layerTexture = null; |
| this._update(); |
| return; |
| } |
| UI.UIUtils.loadImage(imageURL).then(image => { |
| const texture = image && LayerTextureManager._createTextureForImage(this._gl || null, image); |
| this._layerTexture = texture ? {layer: layer, texture: texture} : null; |
| this._update(); |
| }); |
| } |
| |
| onResize(): void { |
| this._resizeCanvas(); |
| this._update(); |
| } |
| |
| willHide(): void { |
| this._textureManager.suspend(); |
| } |
| |
| wasShown(): void { |
| this._textureManager.resume(); |
| if (!this._needsUpdate) { |
| return; |
| } |
| this._resizeCanvas(); |
| this._update(); |
| } |
| |
| updateLayerSnapshot(layer: SDK.LayerTreeBase.Layer): void { |
| this._textureManager.layerNeedsUpdate(layer); |
| } |
| |
| _setOutline(type: OutlineType, selection: Selection|null): void { |
| this._lastSelection[type] = selection; |
| this._update(); |
| } |
| |
| hoverObject(selection: Selection|null): void { |
| this._setOutline(OutlineType.Hovered, selection); |
| } |
| |
| selectObject(selection: Selection|null): void { |
| this._setOutline(OutlineType.Hovered, null); |
| this._setOutline(OutlineType.Selected, selection); |
| } |
| |
| snapshotForSelection(selection: Selection): Promise<SDK.PaintProfiler.SnapshotWithRect|null> { |
| if (selection.type() === Type.Snapshot) { |
| const snapshotWithRect = (selection as SnapshotSelection).snapshot(); |
| snapshotWithRect.snapshot.addReference(); |
| return /** @type {!Promise<?SDK.PaintProfiler.SnapshotWithRect>} */ Promise.resolve(snapshotWithRect) as |
| Promise<SDK.PaintProfiler.SnapshotWithRect|null>; |
| } |
| if (selection.layer()) { |
| const promise = selection.layer().snapshots()[0]; |
| if (promise) { |
| return promise; |
| } |
| } |
| return /** @type {!Promise<?SDK.PaintProfiler.SnapshotWithRect>} */ Promise.resolve(null) as |
| Promise<SDK.PaintProfiler.SnapshotWithRect|null>; |
| } |
| |
| _initGL(canvas: HTMLCanvasElement): WebGLRenderingContext|null { |
| const gl = canvas.getContext('webgl'); |
| if (!gl) { |
| return null; |
| } |
| gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
| gl.enable(gl.BLEND); |
| gl.clearColor(0.0, 0.0, 0.0, 0.0); |
| gl.enable(gl.DEPTH_TEST); |
| return /** @type {!WebGLRenderingContext} */ gl as WebGLRenderingContext; |
| } |
| |
| _createShader(type: number, script: string): void { |
| if (!this._gl) { |
| return; |
| } |
| |
| const shader = this._gl.createShader(type); |
| if (shader && this._shaderProgram) { |
| this._gl.shaderSource(shader, script); |
| this._gl.compileShader(shader); |
| this._gl.attachShader(this._shaderProgram, shader); |
| } |
| } |
| |
| _initShaders(): void { |
| if (!this._gl) { |
| return; |
| } |
| |
| this._shaderProgram = this._gl.createProgram(); |
| if (!this._shaderProgram) { |
| return; |
| } |
| this._createShader(this._gl.FRAGMENT_SHADER, FragmentShader); |
| this._createShader(this._gl.VERTEX_SHADER, VertexShader); |
| this._gl.linkProgram(this._shaderProgram); |
| this._gl.useProgram(this._shaderProgram); |
| |
| const aVertexPositionAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aVertexPosition'); |
| this._gl.enableVertexAttribArray(aVertexPositionAttribute); |
| vertexPositionAttributes.set(this._shaderProgram, aVertexPositionAttribute); |
| |
| const aVertexColorAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aVertexColor'); |
| this._gl.enableVertexAttribArray(aVertexColorAttribute); |
| vertexColorAttributes.set(this._shaderProgram, aVertexColorAttribute); |
| |
| const aTextureCoordAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aTextureCoord'); |
| this._gl.enableVertexAttribArray(aTextureCoordAttribute); |
| textureCoordAttributes.set(this._shaderProgram, aTextureCoordAttribute); |
| |
| const uMatrixLocation = this._gl.getUniformLocation(this._shaderProgram, 'uPMatrix'); |
| uniformMatrixLocations.set(this._shaderProgram, uMatrixLocation); |
| |
| const uSamplerLocation = this._gl.getUniformLocation(this._shaderProgram, 'uSampler'); |
| uniformSamplerLocations.set(this._shaderProgram, uSamplerLocation); |
| } |
| |
| _resizeCanvas(): void { |
| this._canvasElement.width = this._canvasElement.offsetWidth * window.devicePixelRatio; |
| this._canvasElement.height = this._canvasElement.offsetHeight * window.devicePixelRatio; |
| } |
| |
| _updateTransformAndConstraints(): void { |
| const paddingFraction = 0.1; |
| const dimensionsForAutoscale = this._dimensionsForAutoscale || {width: 0, height: 0}; |
| const viewport = this._layerTree ? this._layerTree.viewportSize() : null; |
| const baseWidth = viewport ? viewport.width : dimensionsForAutoscale.width; |
| const baseHeight = viewport ? viewport.height : dimensionsForAutoscale.height; |
| const canvasWidth = this._canvasElement.width; |
| const canvasHeight = this._canvasElement.height; |
| const paddingX = canvasWidth * paddingFraction; |
| const paddingY = canvasHeight * paddingFraction; |
| const scaleX = (canvasWidth - 2 * paddingX) / baseWidth; |
| const scaleY = (canvasHeight - 2 * paddingY) / baseHeight; |
| const viewScale = Math.min(scaleX, scaleY); |
| const minScaleConstraint = |
| Math.min(baseWidth / dimensionsForAutoscale.width, baseHeight / dimensionsForAutoscale.width) / 2; |
| this._transformController.setScaleConstraints( |
| minScaleConstraint, |
| 10 / viewScale); // 1/viewScale is 1:1 in terms of pixels, so allow zooming to 10x of native size |
| const scale = this._transformController.scale(); |
| const rotateX = this._transformController.rotateX(); |
| const rotateY = this._transformController.rotateY(); |
| |
| this._scale = scale * viewScale; |
| const textureScale = Platform.NumberUtilities.clamp(this._scale, 0.1, 1); |
| if (textureScale !== this._oldTextureScale) { |
| this._oldTextureScale = textureScale; |
| this._textureManager.setScale(textureScale); |
| this.dispatchEventToListeners(Events.ScaleChanged, textureScale); |
| } |
| const scaleAndRotationMatrix = new WebKitCSSMatrix() |
| .scale(scale, scale, scale) |
| .translate(canvasWidth / 2, canvasHeight / 2, 0) |
| .rotate(rotateX, rotateY, 0) |
| .scale(viewScale, viewScale, viewScale) |
| .translate(-baseWidth / 2, -baseHeight / 2, 0); |
| |
| let bounds; |
| for (let i = 0; i < this._rects.length; ++i) { |
| bounds = UI.Geometry.boundsForTransformedPoints(scaleAndRotationMatrix, this._rects[i].vertices, bounds); |
| } |
| |
| if (bounds) { |
| this._transformController.clampOffsets( |
| (paddingX - bounds.maxX) / window.devicePixelRatio, |
| (canvasWidth - paddingX - bounds.minX) / window.devicePixelRatio, |
| (paddingY - bounds.maxY) / window.devicePixelRatio, |
| (canvasHeight - paddingY - bounds.minY) / window.devicePixelRatio); |
| } |
| const offsetX = this._transformController.offsetX() * window.devicePixelRatio; |
| const offsetY = this._transformController.offsetY() * window.devicePixelRatio; |
| // Multiply to translation matrix on the right rather than translate (which would implicitly multiply on the left). |
| this._projectionMatrix = new WebKitCSSMatrix().translate(offsetX, offsetY, 0).multiply(scaleAndRotationMatrix); |
| |
| const glProjectionMatrix = new WebKitCSSMatrix() |
| .scale(1, -1, -1) |
| .translate(-1, -1, 0) |
| .scale(2 / this._canvasElement.width, 2 / this._canvasElement.height, 1 / 1000000) |
| .multiply(this._projectionMatrix); |
| |
| if (this._shaderProgram) { |
| const pMatrixUniform = uniformMatrixLocations.get(this._shaderProgram); |
| if (this._gl && pMatrixUniform) { |
| this._gl.uniformMatrix4fv(pMatrixUniform, false, this._arrayFromMatrix(glProjectionMatrix)); |
| } |
| } |
| } |
| |
| _arrayFromMatrix(m: DOMMatrix): Float32Array { |
| return new Float32Array([ |
| m.m11, |
| m.m12, |
| m.m13, |
| m.m14, |
| m.m21, |
| m.m22, |
| m.m23, |
| m.m24, |
| m.m31, |
| m.m32, |
| m.m33, |
| m.m34, |
| m.m41, |
| m.m42, |
| m.m43, |
| m.m44, |
| ]); |
| } |
| |
| _initWhiteTexture(): void { |
| if (!this._gl) { |
| return; |
| } |
| |
| this._whiteTexture = this._gl.createTexture(); |
| this._gl.bindTexture(this._gl.TEXTURE_2D, this._whiteTexture); |
| const whitePixel = new Uint8Array([255, 255, 255, 255]); |
| this._gl.texImage2D( |
| this._gl.TEXTURE_2D, 0, this._gl.RGBA, 1, 1, 0, this._gl.RGBA, this._gl.UNSIGNED_BYTE, whitePixel); |
| } |
| |
| _initChromeTextures(): void { |
| function loadChromeTexture(this: Layers3DView, index: ChromeTexture, url: string): void { |
| UI.UIUtils.loadImage(url).then(image => { |
| this._chromeTextures[index] = |
| image && LayerTextureManager._createTextureForImage(this._gl || null, image) || undefined; |
| }); |
| } |
| loadChromeTexture.call(this, ChromeTexture.Left, 'Images/chromeLeft.avif'); |
| loadChromeTexture.call(this, ChromeTexture.Middle, 'Images/chromeMiddle.avif'); |
| loadChromeTexture.call(this, ChromeTexture.Right, 'Images/chromeRight.avif'); |
| } |
| |
| _initGLIfNecessary(): WebGLRenderingContext|null { |
| if (this._gl) { |
| return this._gl; |
| } |
| this._gl = this._initGL(this._canvasElement); |
| if (!this._gl) { |
| return null; |
| } |
| this._initShaders(); |
| this._initWhiteTexture(); |
| this._initChromeTextures(); |
| this._textureManager.setContext(this._gl); |
| return this._gl; |
| } |
| |
| _calculateDepthsAndVisibility(): void { |
| /** @type {!Map<string, number>} */ |
| this._depthByLayerId = new Map(); |
| let depth = 0; |
| const showInternalLayers = this._layerViewHost.showInternalLayersSetting().get(); |
| if (!this._layerTree) { |
| return; |
| } |
| |
| const root = |
| showInternalLayers ? this._layerTree.root() : (this._layerTree.contentRoot() || this._layerTree.root()); |
| if (!root) { |
| return; |
| } |
| |
| const queue = [root]; |
| this._depthByLayerId.set(root.id(), 0); |
| /** @type {!Set<!SDK.LayerTreeBase.Layer>} */ |
| this._visibleLayers = new Set(); |
| while (queue.length > 0) { |
| const layer = queue.shift(); |
| if (!layer) { |
| break; |
| } |
| |
| if (showInternalLayers || layer.drawsContent()) { |
| this._visibleLayers.add(layer); |
| } |
| const children = layer.children(); |
| for (let i = 0; i < children.length; ++i) { |
| this._depthByLayerId.set(children[i].id(), ++depth); |
| queue.push(children[i]); |
| } |
| } |
| this._maxDepth = depth; |
| } |
| |
| _depthForLayer(layer: SDK.LayerTreeBase.Layer): number { |
| return (this._depthByLayerId.get(layer.id()) || 0) * LayerSpacing; |
| } |
| |
| _calculateScrollRectDepth(layer: SDK.LayerTreeBase.Layer, index: number): number { |
| return this._depthForLayer(layer) + index * ScrollRectSpacing + 1; |
| } |
| |
| _updateDimensionsForAutoscale(layer: SDK.LayerTreeBase.Layer): void { |
| // We don't want to be precise, but rather pick something least affected by |
| // animationtransforms, so that we don't change scale too often. So let's |
| // disregard transforms, scrolling and relative layer positioning and choose |
| // the largest dimensions of all layers. |
| if (!this._dimensionsForAutoscale) { |
| this._dimensionsForAutoscale = {width: 0, height: 0}; |
| } |
| |
| this._dimensionsForAutoscale.width = Math.max(layer.width(), this._dimensionsForAutoscale.width); |
| this._dimensionsForAutoscale.height = Math.max(layer.height(), this._dimensionsForAutoscale.height); |
| } |
| |
| _calculateLayerRect(layer: SDK.LayerTreeBase.Layer): void { |
| if (!this._visibleLayers.has(layer)) { |
| return; |
| } |
| const selection = new LayerSelection(layer); |
| const rect = new Rectangle(selection); |
| rect.setVertices(layer.quad(), this._depthForLayer(layer)); |
| this._appendRect(rect); |
| this._updateDimensionsForAutoscale(layer); |
| } |
| |
| _appendRect(rect: Rectangle): void { |
| const selection = rect.relatedObject; |
| const isSelected = Selection.isEqual(this._lastSelection[OutlineType.Selected], selection); |
| const isHovered = Selection.isEqual(this._lastSelection[OutlineType.Hovered], selection); |
| if (isSelected) { |
| rect.borderColor = SelectedBorderColor; |
| } else if (isHovered) { |
| rect.borderColor = HoveredBorderColor; |
| const fillColor = rect.fillColor || [255, 255, 255, 1]; |
| const maskColor = HoveredImageMaskColor; |
| rect.fillColor = [ |
| fillColor[0] * maskColor[0] / 255, |
| fillColor[1] * maskColor[1] / 255, |
| fillColor[2] * maskColor[2] / 255, |
| fillColor[3] * maskColor[3], |
| ]; |
| } else { |
| rect.borderColor = BorderColor; |
| } |
| rect.lineWidth = isSelected ? SelectedBorderWidth : BorderWidth; |
| this._rects.push(rect); |
| } |
| |
| _calculateLayerScrollRects(layer: SDK.LayerTreeBase.Layer): void { |
| const scrollRects = layer.scrollRects(); |
| for (let i = 0; i < scrollRects.length; ++i) { |
| const selection = new ScrollRectSelection(layer, i); |
| const rect = new Rectangle(selection); |
| rect.calculateVerticesFromRect(layer, scrollRects[i].rect, this._calculateScrollRectDepth(layer, i)); |
| rect.fillColor = ScrollRectBackgroundColor; |
| this._appendRect(rect); |
| } |
| } |
| |
| _calculateLayerTileRects(layer: SDK.LayerTreeBase.Layer): void { |
| const tiles = this._textureManager.tilesForLayer(layer); |
| for (let i = 0; i < tiles.length; ++i) { |
| const tile = tiles[i]; |
| if (!tile.texture) { |
| continue; |
| } |
| const selection = new SnapshotSelection(layer, {rect: tile.rect, snapshot: tile.snapshot}); |
| const rect = new Rectangle(selection); |
| if (!this._snapshotLayers.has(layer)) { |
| this._snapshotLayers.set(layer, selection); |
| } |
| |
| rect.calculateVerticesFromRect(layer, tile.rect, this._depthForLayer(layer) + 1); |
| rect.texture = tile.texture; |
| this._appendRect(rect); |
| } |
| } |
| |
| _calculateRects(): void { |
| this._rects = []; |
| this._snapshotLayers.clear(); |
| this._dimensionsForAutoscale = {width: 0, height: 0}; |
| if (this._layerTree) { |
| this._layerTree.forEachLayer(this._calculateLayerRect.bind(this)); |
| } |
| |
| if (this._showSlowScrollRectsSetting && this._showSlowScrollRectsSetting.get() && this._layerTree) { |
| this._layerTree.forEachLayer(this._calculateLayerScrollRects.bind(this)); |
| } |
| |
| if (this._layerTexture && this._visibleLayers.has(this._layerTexture.layer)) { |
| const layer = this._layerTexture.layer; |
| const selection = new LayerSelection(layer); |
| const rect = new Rectangle(selection); |
| rect.setVertices(layer.quad(), this._depthForLayer(layer)); |
| rect.texture = this._layerTexture.texture; |
| this._appendRect(rect); |
| } else if (this._showPaints() && this._layerTree) { |
| this._layerTree.forEachLayer(this._calculateLayerTileRects.bind(this)); |
| } |
| } |
| |
| _makeColorsArray(color: number[]): number[] { |
| let colors: number[] = []; |
| const normalizedColor = [color[0] / 255, color[1] / 255, color[2] / 255, color[3]]; |
| for (let i = 0; i < 4; i++) { |
| colors = colors.concat(normalizedColor); |
| } |
| return colors; |
| } |
| |
| _setVertexAttribute(attribute: number, array: number[], length: number): void { |
| const gl = this._gl; |
| if (!gl) { |
| return; |
| } |
| |
| const buffer = gl.createBuffer(); |
| gl.bindBuffer(gl.ARRAY_BUFFER, buffer); |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), gl.STATIC_DRAW); |
| gl.vertexAttribPointer(attribute, length, gl.FLOAT, false, 0, 0); |
| } |
| |
| _drawRectangle(vertices: number[], mode: number, color?: number[], texture?: Object): void { |
| const gl = this._gl; |
| const white = [255, 255, 255, 1]; |
| color = color || white; |
| if (!this._shaderProgram) { |
| return; |
| } |
| |
| const vertexPositionAttribute = vertexPositionAttributes.get(this._shaderProgram); |
| const textureCoordAttribute = textureCoordAttributes.get(this._shaderProgram); |
| const vertexColorAttribute = vertexColorAttributes.get(this._shaderProgram); |
| if (typeof vertexPositionAttribute !== 'undefined') { |
| this._setVertexAttribute(vertexPositionAttribute, vertices, 3); |
| } |
| if (typeof textureCoordAttribute !== 'undefined') { |
| this._setVertexAttribute(textureCoordAttribute, [0, 1, 1, 1, 1, 0, 0, 0], 2); |
| } |
| if (typeof vertexColorAttribute !== 'undefined') { |
| this._setVertexAttribute(vertexColorAttribute, this._makeColorsArray(color), color.length); |
| } |
| |
| if (!gl) { |
| return; |
| } |
| |
| const samplerUniform = uniformSamplerLocations.get(this._shaderProgram); |
| if (texture) { |
| if (samplerUniform) { |
| gl.activeTexture(gl.TEXTURE0); |
| gl.bindTexture(gl.TEXTURE_2D, texture); |
| gl.uniform1i(samplerUniform, 0); |
| } |
| } else if (this._whiteTexture) { |
| gl.bindTexture(gl.TEXTURE_2D, this._whiteTexture); |
| } |
| |
| const numberOfVertices = vertices.length / 3; |
| gl.drawArrays(mode, 0, numberOfVertices); |
| } |
| |
| _drawTexture(vertices: number[], texture: WebGLTexture, color?: number[]): void { |
| if (!this._gl) { |
| return; |
| } |
| |
| this._drawRectangle(vertices, this._gl.TRIANGLE_FAN, color, texture); |
| } |
| |
| _drawViewportAndChrome(): void { |
| if (!this._layerTree) { |
| return; |
| } |
| |
| const viewport = this._layerTree.viewportSize(); |
| if (!viewport) { |
| return; |
| } |
| |
| const drawChrome = !Common.Settings.Settings.instance().moduleSetting('frameViewerHideChromeWindow').get() && |
| this._chromeTextures.length >= 3 && this._chromeTextures.indexOf(undefined) < 0; |
| const z = (this._maxDepth + 1) * LayerSpacing; |
| const borderWidth = Math.ceil(ViewportBorderWidth * this._scale); |
| let vertices: number[] = [viewport.width, 0, z, viewport.width, viewport.height, z, 0, viewport.height, z, 0, 0, z]; |
| if (!this._gl) { |
| return; |
| } |
| |
| this._gl.lineWidth(borderWidth); |
| this._drawRectangle(vertices, drawChrome ? this._gl.LINE_STRIP : this._gl.LINE_LOOP, ViewportBorderColor); |
| |
| if (!drawChrome) { |
| return; |
| } |
| |
| const viewportSize = this._layerTree.viewportSize(); |
| if (!viewportSize) { |
| return; |
| } |
| |
| const borderAdjustment = ViewportBorderWidth / 2; |
| const viewportWidth = viewportSize.width + 2 * borderAdjustment; |
| if (this._chromeTextures[0] && this._chromeTextures[2]) { |
| const chromeTextureImage = |
| imageForTexture.get(this._chromeTextures[0] as WebGLTexture) || {naturalHeight: 0, naturalWidth: 0}; |
| const chromeHeight = chromeTextureImage.naturalHeight; |
| |
| const middleTextureImage = |
| imageForTexture.get(this._chromeTextures[2] as WebGLTexture) || {naturalHeight: 0, naturalWidth: 0}; |
| const middleFragmentWidth = viewportWidth - chromeTextureImage.naturalWidth - middleTextureImage.naturalWidth; |
| let x = -borderAdjustment; |
| const y = -chromeHeight; |
| for (let i = 0; i < this._chromeTextures.length; ++i) { |
| const texture = this._chromeTextures[i]; |
| if (!texture) { |
| continue; |
| } |
| |
| const image = imageForTexture.get(texture); |
| if (!image) { |
| continue; |
| } |
| const width = i === ChromeTexture.Middle ? middleFragmentWidth : image.naturalWidth; |
| if (width < 0 || x + width > viewportWidth) { |
| break; |
| } |
| vertices = [x, y, z, x + width, y, z, x + width, y + chromeHeight, z, x, y + chromeHeight, z]; |
| this._drawTexture(vertices, this._chromeTextures[i] as WebGLTexture); |
| x += width; |
| } |
| } |
| } |
| |
| _drawViewRect(rect: Rectangle): void { |
| if (!this._gl) { |
| return; |
| } |
| |
| const vertices = rect.vertices; |
| if (rect.texture) { |
| this._drawTexture(vertices, rect.texture, rect.fillColor || undefined); |
| } else if (rect.fillColor) { |
| this._drawRectangle(vertices, this._gl.TRIANGLE_FAN, rect.fillColor); |
| } |
| this._gl.lineWidth(rect.lineWidth); |
| if (rect.borderColor) { |
| this._drawRectangle(vertices, this._gl.LINE_LOOP, rect.borderColor); |
| } |
| } |
| |
| _update(): void { |
| if (!this.isShowing()) { |
| this._needsUpdate = true; |
| return; |
| } |
| if (!this._layerTree || !this._layerTree.root()) { |
| this._failBanner.show(this.contentElement); |
| return; |
| } |
| const gl = this._initGLIfNecessary(); |
| if (!gl) { |
| this._failBanner.element.removeChildren(); |
| this._failBanner.element.appendChild(this._webglDisabledBanner()); |
| this._failBanner.show(this.contentElement); |
| return; |
| } |
| this._failBanner.detach(); |
| const viewportWidth = this._canvasElement.width; |
| const viewportHeight = this._canvasElement.height; |
| |
| this._calculateDepthsAndVisibility(); |
| this._calculateRects(); |
| this._updateTransformAndConstraints(); |
| |
| gl.viewport(0, 0, viewportWidth, viewportHeight); |
| gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
| |
| this._rects.forEach(this._drawViewRect.bind(this)); |
| this._drawViewportAndChrome(); |
| } |
| |
| _webglDisabledBanner(): Node { |
| const fragment = this.contentElement.ownerDocument.createDocumentFragment(); |
| fragment.createChild('div').textContent = i18nString(UIStrings.cantDisplayLayers); |
| fragment.createChild('div').textContent = i18nString(UIStrings.webglSupportIsDisabledInYour); |
| fragment.appendChild(i18n.i18n.getFormatLocalizedString( |
| str_, UIStrings.checkSForPossibleReasons, {PH1: UI.XLink.XLink.create('about:gpu')})); |
| return fragment; |
| } |
| |
| _selectionFromEventPoint(event: Event): Selection|null { |
| const mouseEvent = event as MouseEvent; |
| if (!this._layerTree) { |
| return null; |
| } |
| let closestIntersectionPoint: number = Infinity; |
| let closestObject: Selection|null = null; |
| const projectionMatrix = |
| new WebKitCSSMatrix().scale(1, -1, -1).translate(-1, -1, 0).multiply(this._projectionMatrix); |
| const x0 = (mouseEvent.clientX - this._canvasElement.totalOffsetLeft()) * window.devicePixelRatio; |
| const y0 = -(mouseEvent.clientY - this._canvasElement.totalOffsetTop()) * window.devicePixelRatio; |
| |
| function checkIntersection(rect: Rectangle): void { |
| if (!rect.relatedObject) { |
| return; |
| } |
| const t = rect.intersectWithLine(projectionMatrix, x0, y0); |
| if (t && t < closestIntersectionPoint) { |
| closestIntersectionPoint = t; |
| closestObject = rect.relatedObject; |
| } |
| } |
| |
| this._rects.forEach(checkIntersection); |
| return closestObject; |
| } |
| |
| _createVisibilitySetting(caption: string, name: string, value: boolean, toolbar: UI.Toolbar.Toolbar): |
| Common.Settings.Setting<boolean> { |
| const setting = Common.Settings.Settings.instance().createSetting(name, value); |
| setting.setTitle(i18nString(caption)); |
| setting.addChangeListener(this._update, this); |
| toolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox(setting)); |
| return setting; |
| } |
| |
| _initToolbar(): void { |
| this._panelToolbar = this._transformController.toolbar(); |
| this.contentElement.appendChild(this._panelToolbar.element); |
| this._showSlowScrollRectsSetting = this._createVisibilitySetting( |
| i18nString(UIStrings.slowScrollRects), 'frameViewerShowSlowScrollRects', true, this._panelToolbar); |
| this._showPaintsSetting = |
| this._createVisibilitySetting(i18nString(UIStrings.paints), 'frameViewerShowPaints', true, this._panelToolbar); |
| this._showPaintsSetting.addChangeListener(this._updatePaints, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('frameViewerHideChromeWindow') |
| .addChangeListener(this._update, this); |
| } |
| |
| _onContextMenu(event: Event): void { |
| const contextMenu = new UI.ContextMenu.ContextMenu(event); |
| contextMenu.defaultSection().appendItem( |
| i18nString(UIStrings.resetView), () => this._transformController.resetAndNotify(), false); |
| const selection = this._selectionFromEventPoint(event); |
| if (selection && selection.type() === Type.Snapshot) { |
| contextMenu.defaultSection().appendItem( |
| i18nString(UIStrings.showPaintProfiler), |
| this.dispatchEventToListeners.bind(this, Events.PaintProfilerRequested, selection), false); |
| } |
| this._layerViewHost.showContextMenu(contextMenu, selection); |
| } |
| |
| _onMouseMove(event: Event): void { |
| const mouseEvent = event as MouseEvent; |
| if (mouseEvent.which) { |
| return; |
| } |
| this._layerViewHost.hoverObject(this._selectionFromEventPoint(event)); |
| } |
| |
| _onMouseDown(event: Event): void { |
| const mouseEvent = event as MouseEvent; |
| this._mouseDownX = mouseEvent.clientX; |
| this._mouseDownY = mouseEvent.clientY; |
| } |
| |
| _onMouseUp(event: Event): void { |
| const mouseEvent = event as MouseEvent; |
| const maxDistanceInPixels = 6; |
| if (this._mouseDownX && Math.abs(mouseEvent.clientX - this._mouseDownX) < maxDistanceInPixels && |
| Math.abs(mouseEvent.clientY - (this._mouseDownY || 0)) < maxDistanceInPixels) { |
| this._layerViewHost.selectObject(this._selectionFromEventPoint(event)); |
| } |
| delete this._mouseDownX; |
| delete this._mouseDownY; |
| } |
| |
| _onDoubleClick(event: Event): void { |
| const selection = this._selectionFromEventPoint(event); |
| if (selection && (selection.type() === Type.Snapshot || selection.layer())) { |
| this.dispatchEventToListeners(Events.PaintProfilerRequested, selection); |
| } |
| event.stopPropagation(); |
| } |
| |
| _updatePaints(): void { |
| if (this._showPaints()) { |
| this._textureManager.setLayerTree(this._layerTree); |
| this._textureManager.forceUpdate(); |
| } else { |
| this._textureManager.reset(); |
| } |
| this._update(); |
| } |
| |
| _showPaints(): boolean { |
| return this._showPaintsSetting ? this._showPaintsSetting.get() : false; |
| } |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum OutlineType { |
| Hovered = 'hovered', |
| Selected = 'selected', |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum Events { |
| PaintProfilerRequested = 'PaintProfilerRequested', |
| ScaleChanged = 'ScaleChanged', |
| } |
| |
| export const enum ChromeTexture { |
| Left = 0, |
| Middle = 1, |
| Right = 2, |
| } |
| |
| export const FragmentShader = '' + |
| 'precision mediump float;\n' + |
| 'varying vec4 vColor;\n' + |
| 'varying vec2 vTextureCoord;\n' + |
| 'uniform sampler2D uSampler;\n' + |
| 'void main(void)\n' + |
| '{\n' + |
| ' gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)) * vColor;\n' + |
| '}'; |
| |
| export const VertexShader = '' + |
| 'attribute vec3 aVertexPosition;\n' + |
| 'attribute vec2 aTextureCoord;\n' + |
| 'attribute vec4 aVertexColor;\n' + |
| 'uniform mat4 uPMatrix;\n' + |
| 'varying vec2 vTextureCoord;\n' + |
| 'varying vec4 vColor;\n' + |
| 'void main(void)\n' + |
| '{\n' + |
| 'gl_Position = uPMatrix * vec4(aVertexPosition, 1.0);\n' + |
| 'vColor = aVertexColor;\n' + |
| 'vTextureCoord = aTextureCoord;\n' + |
| '}'; |
| |
| export const HoveredBorderColor = [0, 0, 255, 1]; |
| export const SelectedBorderColor = [0, 255, 0, 1]; |
| export const BorderColor = [0, 0, 0, 1]; |
| export const ViewportBorderColor = [160, 160, 160, 1]; |
| export const ScrollRectBackgroundColor = [178, 100, 100, 0.6]; |
| export const HoveredImageMaskColor = [200, 200, 255, 1]; |
| export const BorderWidth = 1; |
| export const SelectedBorderWidth = 2; |
| export const ViewportBorderWidth = 3; |
| |
| export const LayerSpacing = 20; |
| export const ScrollRectSpacing = 4; |
| |
| export class LayerTextureManager { |
| _textureUpdatedCallback: () => void; |
| _throttler: Common.Throttler.Throttler; |
| _scale: number; |
| _active: boolean; |
| _queue!: SDK.LayerTreeBase.Layer[]; |
| _tilesByLayer!: Map<SDK.LayerTreeBase.Layer, Tile[]>; |
| _gl?: WebGLRenderingContext; |
| constructor(textureUpdatedCallback: () => void) { |
| this._textureUpdatedCallback = textureUpdatedCallback; |
| this._throttler = new Common.Throttler.Throttler(0); |
| this._scale = 0; |
| this._active = false; |
| this.reset(); |
| } |
| |
| static _createTextureForImage(gl: WebGLRenderingContext|null, image: HTMLImageElement): WebGLTexture { |
| if (!gl) { |
| throw new Error('WebGLRenderingContext not provided'); |
| } |
| const texture = gl.createTexture(); |
| if (!texture) { |
| throw new Error('Unable to create texture'); |
| } |
| |
| imageForTexture.set(texture, image); |
| gl.bindTexture(gl.TEXTURE_2D, texture); |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); |
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
| gl.bindTexture(gl.TEXTURE_2D, null); |
| return texture; |
| } |
| |
| reset(): void { |
| if (this._tilesByLayer) { |
| this.setLayerTree(null); |
| } |
| |
| /** @type {!Map<!SDK.LayerTreeBase.Layer, !Array<!Tile>>} */ |
| this._tilesByLayer = new Map(); |
| /** @type {!Array<!SDK.LayerTreeBase.Layer>} */ |
| this._queue = []; |
| } |
| |
| setContext(glContext: WebGLRenderingContext): void { |
| this._gl = glContext; |
| if (this._scale) { |
| this._updateTextures(); |
| } |
| } |
| |
| suspend(): void { |
| this._active = false; |
| } |
| |
| resume(): void { |
| this._active = true; |
| if (this._queue.length) { |
| this._update(); |
| } |
| } |
| |
| setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void { |
| const newLayers = new Set<SDK.LayerTreeBase.Layer>(); |
| const oldLayers = Array.from(this._tilesByLayer.keys()); |
| if (layerTree) { |
| layerTree.forEachLayer(layer => { |
| if (!layer.drawsContent()) { |
| return; |
| } |
| newLayers.add(layer); |
| if (!this._tilesByLayer.has(layer)) { |
| this._tilesByLayer.set(layer, []); |
| this.layerNeedsUpdate(layer); |
| } |
| }); |
| } |
| if (!oldLayers.length) { |
| this.forceUpdate(); |
| } |
| for (const layer of oldLayers) { |
| if (newLayers.has(layer)) { |
| continue; |
| } |
| const tiles = this._tilesByLayer.get(layer); |
| if (tiles) { |
| tiles.forEach(tile => tile.dispose()); |
| } |
| this._tilesByLayer.delete(layer); |
| } |
| } |
| |
| _setSnapshotsForLayer(layer: SDK.LayerTreeBase.Layer, snapshots: SDK.PaintProfiler.SnapshotWithRect[]): |
| Promise<void> { |
| const oldSnapshotsToTiles = new Map((this._tilesByLayer.get(layer) || []).map(tile => [tile.snapshot, tile])); |
| const newTiles = []; |
| const reusedTiles = []; |
| for (const snapshot of snapshots) { |
| const oldTile = oldSnapshotsToTiles.get(snapshot.snapshot); |
| if (oldTile) { |
| reusedTiles.push(oldTile); |
| oldSnapshotsToTiles.delete(snapshot.snapshot); |
| } else { |
| newTiles.push(new Tile(snapshot)); |
| } |
| } |
| this._tilesByLayer.set(layer, reusedTiles.concat(newTiles)); |
| for (const tile of oldSnapshotsToTiles.values()) { |
| tile.dispose(); |
| } |
| const gl = this._gl; |
| if (!gl || !this._scale) { |
| return Promise.resolve(); |
| } |
| return Promise.all(newTiles.map(tile => tile.update(gl, this._scale))).then(this._textureUpdatedCallback); |
| } |
| |
| setScale(scale: number): void { |
| if (this._scale && this._scale >= scale) { |
| return; |
| } |
| this._scale = scale; |
| this._updateTextures(); |
| } |
| |
| tilesForLayer(layer: SDK.LayerTreeBase.Layer): Tile[] { |
| return this._tilesByLayer.get(layer) || []; |
| } |
| |
| layerNeedsUpdate(layer: SDK.LayerTreeBase.Layer): void { |
| if (this._queue.indexOf(layer) < 0) { |
| this._queue.push(layer); |
| } |
| if (this._active) { |
| this._throttler.schedule(this._update.bind(this)); |
| } |
| } |
| |
| forceUpdate(): void { |
| this._queue.forEach(layer => this._updateLayer(layer)); |
| this._queue = []; |
| this._update(); |
| } |
| |
| _update(): Promise<void> { |
| const layer = this._queue.shift(); |
| if (!layer) { |
| return Promise.resolve(); |
| } |
| if (this._queue.length) { |
| this._throttler.schedule(this._update.bind(this)); |
| } |
| return this._updateLayer(layer); |
| } |
| |
| _updateLayer(layer: SDK.LayerTreeBase.Layer): Promise<void> { |
| return Promise.all(layer.snapshots()) |
| .then( |
| snapshots => this._setSnapshotsForLayer( |
| layer, snapshots.filter(snapshot => snapshot !== null) as SDK.PaintProfiler.SnapshotWithRect[])); |
| } |
| |
| _updateTextures(): void { |
| if (!this._gl) { |
| return; |
| } |
| if (!this._scale) { |
| return; |
| } |
| |
| for (const tiles of this._tilesByLayer.values()) { |
| for (const tile of tiles) { |
| const promise = tile.updateScale(this._gl, this._scale); |
| if (promise) { |
| promise.then(this._textureUpdatedCallback); |
| } |
| } |
| } |
| } |
| } |
| |
| export class Rectangle { |
| relatedObject: Selection|null; |
| lineWidth: number; |
| borderColor: number[]|null; |
| fillColor: number[]|null; |
| texture: WebGLTexture|null; |
| vertices!: number[]; |
| constructor(relatedObject: Selection|null) { |
| this.relatedObject = relatedObject; |
| this.lineWidth = 1; |
| this.borderColor = null; |
| this.fillColor = null; |
| this.texture = null; |
| } |
| |
| setVertices(quad: number[], z: number): void { |
| this.vertices = [quad[0], quad[1], z, quad[2], quad[3], z, quad[4], quad[5], z, quad[6], quad[7], z]; |
| } |
| |
| /** |
| * Finds coordinates of point on layer quad, having offsets (ratioX * width) and (ratioY * height) |
| * from the left corner of the initial layer rect, where width and heigth are layer bounds. |
| */ |
| _calculatePointOnQuad(quad: number[], ratioX: number, ratioY: number): number[] { |
| const x0 = quad[0]; |
| const y0 = quad[1]; |
| const x1 = quad[2]; |
| const y1 = quad[3]; |
| const x2 = quad[4]; |
| const y2 = quad[5]; |
| const x3 = quad[6]; |
| const y3 = quad[7]; |
| // Point on the first quad side clockwise |
| const firstSidePointX = x0 + ratioX * (x1 - x0); |
| const firstSidePointY = y0 + ratioX * (y1 - y0); |
| // Point on the third quad side clockwise |
| const thirdSidePointX = x3 + ratioX * (x2 - x3); |
| const thirdSidePointY = y3 + ratioX * (y2 - y3); |
| const x = firstSidePointX + ratioY * (thirdSidePointX - firstSidePointX); |
| const y = firstSidePointY + ratioY * (thirdSidePointY - firstSidePointY); |
| return [x, y]; |
| } |
| |
| calculateVerticesFromRect(layer: SDK.LayerTreeBase.Layer, rect: Protocol.DOM.Rect, z: number): void { |
| const quad = layer.quad(); |
| const rx1 = rect.x / layer.width(); |
| const rx2 = (rect.x + rect.width) / layer.width(); |
| const ry1 = rect.y / layer.height(); |
| const ry2 = (rect.y + rect.height) / layer.height(); |
| const rectQuad = this._calculatePointOnQuad(quad, rx1, ry1) |
| .concat(this._calculatePointOnQuad(quad, rx2, ry1)) |
| .concat(this._calculatePointOnQuad(quad, rx2, ry2)) |
| .concat(this._calculatePointOnQuad(quad, rx1, ry2)); |
| this.setVertices(rectQuad, z); |
| } |
| |
| /** |
| * Intersects quad with given transform matrix and line l(t) = (x0, y0, t) |
| */ |
| intersectWithLine(matrix: DOMMatrix, x0: number, y0: number): number|undefined { |
| let i; |
| // Vertices of the quad with transform matrix applied |
| const points = []; |
| for (i = 0; i < 4; ++i) { |
| points[i] = UI.Geometry.multiplyVectorByMatrixAndNormalize( |
| new UI.Geometry.Vector(this.vertices[i * 3], this.vertices[i * 3 + 1], this.vertices[i * 3 + 2]), matrix); |
| } |
| // Calculating quad plane normal |
| const normal = UI.Geometry.crossProduct( |
| UI.Geometry.subtract(points[1], points[0]), UI.Geometry.subtract(points[2], points[1])); |
| // General form of the equation of the quad plane: A * x + B * y + C * z + D = 0 |
| const A = normal.x; |
| const B = normal.y; |
| const C = normal.z; |
| const D = -(A * points[0].x + B * points[0].y + C * points[0].z); |
| // Finding t from the equation |
| const t = -(D + A * x0 + B * y0) / C; |
| // Point of the intersection |
| const pt = new UI.Geometry.Vector(x0, y0, t); |
| // Vectors from the intersection point to vertices of the quad |
| const tVects = points.map(UI.Geometry.subtract.bind(null, pt)); |
| // Intersection point lies inside of the polygon if scalar products of normal of the plane and |
| // cross products of successive tVects are all nonstrictly above or all nonstrictly below zero |
| for (i = 0; i < tVects.length; ++i) { |
| const product = |
| UI.Geometry.scalarProduct(normal, UI.Geometry.crossProduct(tVects[i], tVects[(i + 1) % tVects.length])); |
| if (product < 0) { |
| return undefined; |
| } |
| } |
| return t; |
| } |
| } |
| |
| export class Tile { |
| snapshot: SDK.PaintProfiler.PaintProfilerSnapshot; |
| rect: Protocol.DOM.Rect; |
| scale: number; |
| texture: WebGLTexture|null; |
| _gl!: WebGLRenderingContext; |
| constructor(snapshotWithRect: SDK.PaintProfiler.SnapshotWithRect) { |
| this.snapshot = snapshotWithRect.snapshot; |
| this.rect = snapshotWithRect.rect; |
| this.scale = 0; |
| this.texture = null; |
| } |
| |
| dispose(): void { |
| this.snapshot.release(); |
| if (this.texture) { |
| this._gl.deleteTexture(this.texture); |
| this.texture = null; |
| } |
| } |
| |
| updateScale(glContext: WebGLRenderingContext, scale: number): Promise<void>|null { |
| if (this.texture && this.scale >= scale) { |
| return null; |
| } |
| return this.update(glContext, scale); |
| } |
| |
| async update(glContext: WebGLRenderingContext, scale: number): Promise<void> { |
| this._gl = glContext; |
| this.scale = scale; |
| const imageURL = await this.snapshot.replay(scale); |
| const image = imageURL ? await UI.UIUtils.loadImage(imageURL) : null; |
| this.texture = image ? LayerTextureManager._createTextureForImage(glContext, image) : null; |
| } |
| } |