blob: 09eb9a481b097e5b613be99976aa676ea985ae55 [file] [log] [blame]
/*
* 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.
*/
/**
* @implements {LayerViewer.LayerView}
* @unrestricted
*/
export class Layers3DView extends UI.VBox {
/**
* @param {!LayerViewer.LayerViewHost} layerViewHost
*/
constructor(layerViewHost) {
super(true);
this.registerRequiredCSS('layer_viewer/layers3DView.css');
this.contentElement.classList.add('layers-3d-view');
this._failBanner = new UI.VBox();
this._failBanner.element.classList.add('full-widget-dimmed-banner');
this._failBanner.element.createTextChild(Common.UIString('Layer information is not yet available.'));
this._layerViewHost = layerViewHost;
this._layerViewHost.registerView(this);
this._transformController = new LayerViewer.TransformController(this.contentElement);
this._transformController.addEventListener(
LayerViewer.TransformController.Events.TransformChanged, this._update, this);
this._initToolbar();
this._canvasElement = this.contentElement.createChild('canvas');
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, ls`3D Layers View`);
this._lastSelection = {};
this._layerTree = null;
this._textureManager = new LayerTextureManager(this._update.bind(this));
/** @type Array.<!WebGLTexture|undefined> */
this._chromeTextures = [];
this._rects = [];
/** @type Map<SDK.Layer, LayerViewer.LayerView.SnapshotSelection> */
this._snapshotLayers = new Map();
this._layerViewHost.setLayerSnapshotMap(this._snapshotLayers);
this._layerViewHost.showInternalLayersSetting().addChangeListener(this._update, this);
}
/**
* @param {?SDK.LayerTreeBase} layerTree
* @override
*/
setLayerTree(layerTree) {
this._layerTree = layerTree;
this._layerTexture = null;
delete this._oldTextureScale;
if (this._showPaints()) {
this._textureManager.setLayerTree(layerTree);
}
this._update();
}
/**
* @param {!SDK.Layer} layer
* @param {string=} imageURL
*/
showImageForLayer(layer, imageURL) {
if (!imageURL) {
this._layerTexture = null;
this._update();
return;
}
UI.loadImage(imageURL).then(image => {
const texture = image && LayerTextureManager._createTextureForImage(this._gl, image);
this._layerTexture = texture ? {layer: layer, texture: texture} : null;
this._update();
});
}
/**
* @override
*/
onResize() {
this._resizeCanvas();
this._update();
}
/**
* @override
*/
willHide() {
this._textureManager.suspend();
}
/**
* @override
*/
wasShown() {
this._textureManager.resume();
if (!this._needsUpdate) {
return;
}
this._resizeCanvas();
this._update();
}
/**
* @param {!SDK.Layer} layer
*/
updateLayerSnapshot(layer) {
this._textureManager.layerNeedsUpdate(layer);
}
/**
* @param {!LayerViewer.Layers3DView.OutlineType} type
* @param {?LayerViewer.LayerView.Selection} selection
*/
_setOutline(type, selection) {
this._lastSelection[type] = selection;
this._update();
}
/**
* @param {?LayerViewer.LayerView.Selection} selection
* @override
*/
hoverObject(selection) {
this._setOutline(OutlineType.Hovered, selection);
}
/**
* @param {?LayerViewer.LayerView.Selection} selection
* @override
*/
selectObject(selection) {
this._setOutline(OutlineType.Hovered, null);
this._setOutline(OutlineType.Selected, selection);
}
/**
* @param {!LayerViewer.LayerView.Selection} selection
* @return {!Promise<?SDK.SnapshotWithRect>}
*/
snapshotForSelection(selection) {
if (selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot) {
const snapshotWithRect = /** @type {!LayerViewer.LayerView.SnapshotSelection} */ (selection).snapshot();
snapshotWithRect.snapshot.addReference();
return /** @type {!Promise<?SDK.SnapshotWithRect>} */ (Promise.resolve(snapshotWithRect));
}
if (selection.layer()) {
const promise = selection.layer().snapshots()[0];
if (promise) {
return promise;
}
}
return /** @type {!Promise<?SDK.SnapshotWithRect>} */ (Promise.resolve(null));
}
/**
* @param {!Element} canvas
* @return {?WebGLRenderingContext}
*/
_initGL(canvas) {
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 gl;
}
/**
* @param {!Object} type
* @param {string} script
*/
_createShader(type, script) {
const shader = this._gl.createShader(type);
this._gl.shaderSource(shader, script);
this._gl.compileShader(shader);
this._gl.attachShader(this._shaderProgram, shader);
}
_initShaders() {
this._shaderProgram = this._gl.createProgram();
this._createShader(this._gl.FRAGMENT_SHADER, FragmentShader);
this._createShader(this._gl.VERTEX_SHADER, VertexShader);
this._gl.linkProgram(this._shaderProgram);
this._gl.useProgram(this._shaderProgram);
this._shaderProgram.vertexPositionAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aVertexPosition');
this._gl.enableVertexAttribArray(this._shaderProgram.vertexPositionAttribute);
this._shaderProgram.vertexColorAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aVertexColor');
this._gl.enableVertexAttribArray(this._shaderProgram.vertexColorAttribute);
this._shaderProgram.textureCoordAttribute = this._gl.getAttribLocation(this._shaderProgram, 'aTextureCoord');
this._gl.enableVertexAttribArray(this._shaderProgram.textureCoordAttribute);
this._shaderProgram.pMatrixUniform = this._gl.getUniformLocation(this._shaderProgram, 'uPMatrix');
this._shaderProgram.samplerUniform = this._gl.getUniformLocation(this._shaderProgram, 'uSampler');
}
_resizeCanvas() {
this._canvasElement.width = this._canvasElement.offsetWidth * window.devicePixelRatio;
this._canvasElement.height = this._canvasElement.offsetHeight * window.devicePixelRatio;
}
_updateTransformAndConstraints() {
const paddingFraction = 0.1;
const viewport = this._layerTree.viewportSize();
const baseWidth = viewport ? viewport.width : this._dimensionsForAutoscale.width;
const baseHeight = viewport ? viewport.height : this._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 / this._dimensionsForAutoscale.width, baseHeight / this._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 = Number.constrain(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);
}
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);
this._gl.uniformMatrix4fv(this._shaderProgram.pMatrixUniform, false, this._arrayFromMatrix(glProjectionMatrix));
}
/**
* @param {!CSSMatrix} m
* @return {!Float32Array}
*/
_arrayFromMatrix(m) {
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() {
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() {
/**
* @this {LayerViewer.Layers3DView}
* @param {!LayerViewer.Layers3DView.ChromeTexture} index
* @param {string} url
*/
function loadChromeTexture(index, url) {
UI.loadImage(url).then(image => {
this._chromeTextures[index] = image && LayerTextureManager._createTextureForImage(this._gl, image) || undefined;
});
}
loadChromeTexture.call(this, ChromeTexture.Left, 'Images/chromeLeft.png');
loadChromeTexture.call(this, ChromeTexture.Middle, 'Images/chromeMiddle.png');
loadChromeTexture.call(this, ChromeTexture.Right, 'Images/chromeRight.png');
}
/**
* @return {?WebGLRenderingContext}
*/
_initGLIfNecessary() {
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() {
this._depthByLayerId = {};
let depth = 0;
const showInternalLayers = this._layerViewHost.showInternalLayersSetting().get();
const root =
showInternalLayers ? this._layerTree.root() : (this._layerTree.contentRoot() || this._layerTree.root());
const queue = [root];
this._depthByLayerId[root.id()] = 0;
/** @type {!Set<!SDK.Layer>} */
this._visibleLayers = new Set();
while (queue.length > 0) {
const layer = queue.shift();
if (showInternalLayers || layer.drawsContent()) {
this._visibleLayers.add(layer);
}
const children = layer.children();
for (let i = 0; i < children.length; ++i) {
this._depthByLayerId[children[i].id()] = ++depth;
queue.push(children[i]);
}
}
this._maxDepth = depth;
}
/**
* @param {!SDK.Layer} layer
* @return {number}
*/
_depthForLayer(layer) {
return this._depthByLayerId[layer.id()] * LayerSpacing;
}
/**
* @param {!SDK.Layer} layer
* @param {number} index
* @return {number}
*/
_calculateScrollRectDepth(layer, index) {
return this._depthForLayer(layer) + index * ScrollRectSpacing + 1;
}
/**
* @param {!SDK.Layer} layer
*/
_updateDimensionsForAutoscale(layer) {
// 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.
this._dimensionsForAutoscale.width = Math.max(layer.width(), this._dimensionsForAutoscale.width);
this._dimensionsForAutoscale.height = Math.max(layer.height(), this._dimensionsForAutoscale.height);
}
/**
* @param {!SDK.Layer} layer
*/
_calculateLayerRect(layer) {
if (!this._visibleLayers.has(layer)) {
return;
}
const selection = new LayerViewer.LayerView.LayerSelection(layer);
const rect = new Rectangle(selection);
rect.setVertices(layer.quad(), this._depthForLayer(layer));
this._appendRect(rect);
this._updateDimensionsForAutoscale(layer);
}
/**
* @param {!LayerViewer.Layers3DView.Rectangle} rect
*/
_appendRect(rect) {
const selection = rect.relatedObject;
const isSelected = LayerViewer.LayerView.Selection.isEqual(this._lastSelection[OutlineType.Selected], selection);
const isHovered = LayerViewer.LayerView.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);
}
/**
* @param {!SDK.Layer} layer
*/
_calculateLayerScrollRects(layer) {
const scrollRects = layer.scrollRects();
for (let i = 0; i < scrollRects.length; ++i) {
const selection = new LayerViewer.LayerView.ScrollRectSelection(layer, i);
const rect = new Rectangle(selection);
rect.calculateVerticesFromRect(layer, scrollRects[i].rect, this._calculateScrollRectDepth(layer, i));
rect.fillColor = ScrollRectBackgroundColor;
this._appendRect(rect);
}
}
/**
* @param {!SDK.Layer} layer
*/
_calculateLayerTileRects(layer) {
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 LayerViewer.LayerView.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() {
this._rects = [];
this._snapshotLayers.clear();
this._dimensionsForAutoscale = {width: 0, height: 0};
this._layerTree.forEachLayer(this._calculateLayerRect.bind(this));
if (this._showSlowScrollRectsSetting.get()) {
this._layerTree.forEachLayer(this._calculateLayerScrollRects.bind(this));
}
if (this._layerTexture && this._visibleLayers.has(this._layerTexture.layer)) {
const layer = this._layerTexture.layer;
const selection = new LayerViewer.LayerView.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.forEachLayer(this._calculateLayerTileRects.bind(this));
}
}
/**
* @param {!Array.<number>} color
* @return {!Array.<number>}
*/
_makeColorsArray(color) {
let colors = [];
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;
}
/**
* @param {!Object} attribute
* @param {!Array.<number>} array
* @param {number} length
*/
_setVertexAttribute(attribute, array, length) {
const gl = this._gl;
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);
}
/**
* @param {!Array.<number>} vertices
* @param {number} mode
* @param {!Array.<number>=} color
* @param {!Object=} texture
*/
_drawRectangle(vertices, mode, color, texture) {
const gl = this._gl;
const white = [255, 255, 255, 1];
color = color || white;
this._setVertexAttribute(this._shaderProgram.vertexPositionAttribute, vertices, 3);
this._setVertexAttribute(this._shaderProgram.textureCoordAttribute, [0, 1, 1, 1, 1, 0, 0, 0], 2);
this._setVertexAttribute(this._shaderProgram.vertexColorAttribute, this._makeColorsArray(color), color.length);
if (texture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(this._shaderProgram.samplerUniform, 0);
} else {
gl.bindTexture(gl.TEXTURE_2D, this._whiteTexture);
}
const numberOfVertices = vertices.length / 3;
gl.drawArrays(mode, 0, numberOfVertices);
}
/**
* @param {!Array.<number>} vertices
* @param {!WebGLTexture} texture
* @param {!Array.<number>=} color
*/
_drawTexture(vertices, texture, color) {
this._drawRectangle(vertices, this._gl.TRIANGLE_FAN, color, texture);
}
_drawViewportAndChrome() {
const viewport = this._layerTree.viewportSize();
if (!viewport) {
return;
}
const drawChrome = !Common.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 = [viewport.width, 0, z, viewport.width, viewport.height, z, 0, viewport.height, z, 0, 0, z];
this._gl.lineWidth(borderWidth);
this._drawRectangle(vertices, drawChrome ? this._gl.LINE_STRIP : this._gl.LINE_LOOP, ViewportBorderColor);
if (!drawChrome) {
return;
}
const borderAdjustment = ViewportBorderWidth / 2;
const viewportWidth = this._layerTree.viewportSize().width + 2 * borderAdjustment;
const chromeHeight = this._chromeTextures[0].image.naturalHeight;
const middleFragmentWidth =
viewportWidth - this._chromeTextures[0].image.naturalWidth - this._chromeTextures[2].image.naturalWidth;
let x = -borderAdjustment;
const y = -chromeHeight;
for (let i = 0; i < this._chromeTextures.length; ++i) {
const width = i === ChromeTexture.Middle ? middleFragmentWidth : this._chromeTextures[i].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, /** @type {!WebGLTexture} */ (this._chromeTextures[i]));
x += width;
}
}
/**
* @param {!LayerViewer.Layers3DView.Rectangle} rect
*/
_drawViewRect(rect) {
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() {
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();
this._gl.viewportWidth = this._canvasElement.width;
this._gl.viewportHeight = this._canvasElement.height;
this._calculateDepthsAndVisibility();
this._calculateRects();
this._updateTransformAndConstraints();
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
this._rects.forEach(this._drawViewRect.bind(this));
this._drawViewportAndChrome();
}
/**
* @return {!Node}
*/
_webglDisabledBanner() {
const fragment = this.contentElement.ownerDocument.createDocumentFragment();
fragment.createChild('div').textContent = Common.UIString('Can\'t display layers,');
fragment.createChild('div').textContent = Common.UIString('WebGL support is disabled in your browser.');
fragment.appendChild(UI.formatLocalized('Check %s for possible reasons.', [UI.XLink.create('about:gpu')]));
return fragment;
}
/**
* @param {!Event} event
* @return {?LayerViewer.LayerView.Selection}
*/
_selectionFromEventPoint(event) {
if (!this._layerTree) {
return null;
}
let closestIntersectionPoint = Infinity;
let closestObject = null;
const projectionMatrix =
new WebKitCSSMatrix().scale(1, -1, -1).translate(-1, -1, 0).multiply(this._projectionMatrix);
const x0 = (event.clientX - this._canvasElement.totalOffsetLeft()) * window.devicePixelRatio;
const y0 = -(event.clientY - this._canvasElement.totalOffsetTop()) * window.devicePixelRatio;
/**
* @param {!LayerViewer.Layers3DView.Rectangle} rect
*/
function checkIntersection(rect) {
if (!rect.relatedObject) {
return;
}
const t = rect.intersectWithLine(projectionMatrix, x0, y0);
if (t < closestIntersectionPoint) {
closestIntersectionPoint = t;
closestObject = rect.relatedObject;
}
}
this._rects.forEach(checkIntersection);
return closestObject;
}
/**
* @param {string} caption
* @param {string} name
* @param {boolean} value
* @param {!UI.Toolbar} toolbar
* @return {!Common.Setting}
*/
_createVisibilitySetting(caption, name, value, toolbar) {
const setting = Common.settings.createSetting(name, value);
setting.setTitle(Common.UIString(caption));
setting.addChangeListener(this._update, this);
toolbar.appendToolbarItem(new UI.ToolbarSettingCheckbox(setting));
return setting;
}
_initToolbar() {
this._panelToolbar = this._transformController.toolbar();
this.contentElement.appendChild(this._panelToolbar.element);
this._showSlowScrollRectsSetting = this._createVisibilitySetting(
ls`Slow scroll rects`, 'frameViewerShowSlowScrollRects', true, this._panelToolbar);
this._showPaintsSetting =
this._createVisibilitySetting(ls`Paints`, 'frameViewerShowPaints', true, this._panelToolbar);
this._showPaintsSetting.addChangeListener(this._updatePaints, this);
Common.moduleSetting('frameViewerHideChromeWindow').addChangeListener(this._update, this);
}
/**
* @param {!Event} event
*/
_onContextMenu(event) {
const contextMenu = new UI.ContextMenu(event);
contextMenu.defaultSection().appendItem(
Common.UIString('Reset View'), this._transformController.resetAndNotify.bind(this._transformController), false);
const selection = this._selectionFromEventPoint(event);
if (selection && selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot) {
contextMenu.defaultSection().appendItem(
Common.UIString('Show Paint Profiler'),
this.dispatchEventToListeners.bind(this, Events.PaintProfilerRequested, selection), false);
}
this._layerViewHost.showContextMenu(contextMenu, selection);
}
/**
* @param {!Event} event
*/
_onMouseMove(event) {
if (event.which) {
return;
}
this._layerViewHost.hoverObject(this._selectionFromEventPoint(event));
}
/**
* @param {!Event} event
*/
_onMouseDown(event) {
this._mouseDownX = event.clientX;
this._mouseDownY = event.clientY;
}
/**
* @param {!Event} event
*/
_onMouseUp(event) {
const maxDistanceInPixels = 6;
if (this._mouseDownX && Math.abs(event.clientX - this._mouseDownX) < maxDistanceInPixels &&
Math.abs(event.clientY - this._mouseDownY) < maxDistanceInPixels) {
this._layerViewHost.selectObject(this._selectionFromEventPoint(event));
}
delete this._mouseDownX;
delete this._mouseDownY;
}
/**
* @param {!Event} event
*/
_onDoubleClick(event) {
const selection = this._selectionFromEventPoint(event);
if (selection && (selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot || selection.layer())) {
this.dispatchEventToListeners(Events.PaintProfilerRequested, selection);
}
event.stopPropagation();
}
_updatePaints() {
if (this._showPaints()) {
this._textureManager.setLayerTree(this._layerTree);
this._textureManager.forceUpdate();
} else {
this._textureManager.reset();
}
this._update();
}
/**
* @return {boolean}
*/
_showPaints() {
return this._showPaintsSetting.get();
}
}
/**
* @enum {string}
*/
export const OutlineType = {
Hovered: 'hovered',
Selected: 'selected'
};
/** @enum {symbol} */
export const Events = {
PaintProfilerRequested: Symbol('PaintProfilerRequested'),
ScaleChanged: Symbol('ScaleChanged')
};
/**
* @enum {number}
*/
export const ChromeTexture = {
Left: 0,
Middle: 1,
Right: 2
};
/**
* @enum {string}
*/
export const ScrollRectTitles = {
RepaintsOnScroll: Common.UIString('repaints on scroll'),
TouchEventHandler: Common.UIString('touch event listener'),
WheelEventHandler: Common.UIString('mousewheel event listener')
};
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;
/**
* @unrestricted
*/
export class LayerTextureManager {
/**
* @param {function()} textureUpdatedCallback
*/
constructor(textureUpdatedCallback) {
this._textureUpdatedCallback = textureUpdatedCallback;
this._throttler = new Common.Throttler(0);
this._scale = 0;
this._active = false;
this.reset();
}
/**
* @param {!Image} image
* @param {!WebGLRenderingContext} gl
* @return {!WebGLTexture} texture
*/
static _createTextureForImage(gl, image) {
const texture = gl.createTexture();
texture.image = 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, texture.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() {
if (this._tilesByLayer) {
this.setLayerTree(null);
}
/** @type {!Map<!SDK.Layer, !Array<!LayerViewer.LayerTextureManager.Tile>>} */
this._tilesByLayer = new Map();
/** @type {!Array<!SDK.Layer>} */
this._queue = [];
}
/**
* @param {!WebGLRenderingContext} glContext
*/
setContext(glContext) {
this._gl = glContext;
if (this._scale) {
this._updateTextures();
}
}
suspend() {
this._active = false;
}
resume() {
this._active = true;
if (this._queue.length) {
this._update();
}
}
/**
* @param {?SDK.LayerTreeBase} layerTree
*/
setLayerTree(layerTree) {
const newLayers = new Set();
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;
}
this._tilesByLayer.get(layer).forEach(tile => tile.dispose());
this._tilesByLayer.delete(layer);
}
}
/**
* @param {!SDK.Layer} layer
* @param {!Array<!SDK.SnapshotWithRect>} snapshots
* @return {!Promise}
*/
_setSnapshotsForLayer(layer, snapshots) {
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);
if (oldTile) {
reusedTiles.push(oldTile);
oldSnapshotsToTiles.delete(oldTile);
} else {
newTiles.push(new Tile(snapshot));
}
}
this._tilesByLayer.set(layer, reusedTiles.concat(newTiles));
for (const tile of oldSnapshotsToTiles.values()) {
tile.dispose();
}
if (!this._gl || !this._scale) {
return Promise.resolve();
}
return Promise.all(newTiles.map(tile => tile.update(this._gl, this._scale))).then(this._textureUpdatedCallback);
}
/**
* @param {number} scale
*/
setScale(scale) {
if (this._scale && this._scale >= scale) {
return;
}
this._scale = scale;
this._updateTextures();
}
/**
* @param {!SDK.Layer} layer
* @return {!Array<!LayerViewer.LayerTextureManager.Tile>}
*/
tilesForLayer(layer) {
return this._tilesByLayer.get(layer) || [];
}
/**
* @param {!SDK.Layer} layer
*/
layerNeedsUpdate(layer) {
if (this._queue.indexOf(layer) < 0) {
this._queue.push(layer);
}
if (this._active) {
this._throttler.schedule(this._update.bind(this));
}
}
forceUpdate() {
this._queue.forEach(layer => this._updateLayer(layer));
this._queue = [];
this._update();
}
/**
* @return {!Promise}
*/
_update() {
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);
}
/**
* @param {!SDK.Layer} layer
* @return {!Promise}
*/
_updateLayer(layer) {
return Promise.all(layer.snapshots())
.then(snapshots => this._setSnapshotsForLayer(layer, snapshots.filter(snapshot => !!snapshot)));
}
_updateTextures() {
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);
}
}
}
}
}
/**
* @unrestricted
*/
export class Rectangle {
/**
* @param {?LayerViewer.LayerView.Selection} relatedObject
*/
constructor(relatedObject) {
this.relatedObject = relatedObject;
/** @type {number} */
this.lineWidth = 1;
/** @type {?Array.<number>} */
this.borderColor = null;
/** @type {?Array.<number>} */
this.fillColor = null;
/** @type {?WebGLTexture} */
this.texture = null;
}
/**
* @param {!Array.<number>} quad
* @param {number} z
*/
setVertices(quad, z) {
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.
* @param {!Array.<number>} quad
* @param {number} ratioX
* @param {number} ratioY
* @return {!Array.<number>}
*/
_calculatePointOnQuad(quad, ratioX, ratioY) {
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];
}
/**
* @param {!SDK.Layer} layer
* @param {!Protocol.DOM.Rect} rect
* @param {number} z
*/
calculateVerticesFromRect(layer, rect, z) {
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)
* @param {!CSSMatrix} matrix
* @param {number} x0
* @param {number} y0
* @return {(number|undefined)}
*/
intersectWithLine(matrix, x0, y0) {
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;
}
}
/**
* @unrestricted
*/
export class Tile {
/**
* @param {!SDK.SnapshotWithRect} snapshotWithRect
*/
constructor(snapshotWithRect) {
this.snapshot = snapshotWithRect.snapshot;
this.rect = snapshotWithRect.rect;
this.scale = 0;
/** @type {?WebGLTexture} */
this.texture = null;
}
dispose() {
this.snapshot.release();
if (this.texture) {
this._gl.deleteTexture(this.texture);
this.texture = null;
}
}
/**
* @param {!WebGLRenderingContext} glContext
* @param {number} scale
* @return {?Promise}
*/
updateScale(glContext, scale) {
if (this.texture && this.scale >= scale) {
return null;
}
return this.update(glContext, scale);
}
/**
* @param {!WebGLRenderingContext} glContext
* @param {number} scale
* @return {!Promise}
*/
async update(glContext, scale) {
this._gl = glContext;
this.scale = scale;
const imageURL = await this.snapshot.replay(scale);
const image = imageURL ? await UI.loadImage(imageURL) : null;
this.texture = image ? LayerTextureManager._createTextureForImage(glContext, image) : null;
}
}
/* Legacy exported object */
self.LayerViewer = self.LayerViewer || {};
/* Legacy exported object */
LayerViewer = LayerViewer || {};
/**
* @constructor
*/
LayerViewer.Layers3DView = Layers3DView;
/** @typedef {{borderColor: !Array<number>, borderWidth: number}} */
LayerViewer.Layers3DView.LayerStyle;
/**
* @enum {string}
*/
LayerViewer.Layers3DView.OutlineType = OutlineType;
/**
* @enum {symbol}
*/
LayerViewer.Layers3DView.Events = Events;
/**
* @enum {number}
*/
LayerViewer.Layers3DView.ChromeTexture = ChromeTexture;
/**
* @enum {string}
*/
LayerViewer.Layers3DView.ScrollRectTitles = ScrollRectTitles;
LayerViewer.Layers3DView.FragmentShader = FragmentShader;
LayerViewer.Layers3DView.VertexShader = VertexShader;
LayerViewer.Layers3DView.HoveredBorderColor = HoveredBorderColor;
LayerViewer.Layers3DView.SelectedBorderColor = SelectedBorderColor;
LayerViewer.Layers3DView.BorderColor = BorderColor;
LayerViewer.Layers3DView.ViewportBorderColor = ViewportBorderColor;
LayerViewer.Layers3DView.ScrollRectBackgroundColor = ScrollRectBackgroundColor;
LayerViewer.Layers3DView.HoveredImageMaskColor = HoveredImageMaskColor;
LayerViewer.Layers3DView.BorderWidth = BorderWidth;
LayerViewer.Layers3DView.SelectedBorderWidth = SelectedBorderWidth;
LayerViewer.Layers3DView.ViewportBorderWidth = ViewportBorderWidth;
LayerViewer.Layers3DView.LayerSpacing = LayerSpacing;
LayerViewer.Layers3DView.ScrollRectSpacing = ScrollRectSpacing;
/**
* @constructor
*/
LayerViewer.Layers3DView.Rectangle = Rectangle;
/**
* @constructor
*/
LayerViewer.LayerTextureManager = LayerTextureManager;
/**
* @constructor
*/
LayerViewer.LayerTextureManager.Tile = Tile;