| /* |
| * Copyright (C) 2017 Sony Interactive Entertainment Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. 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. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. |
| */ |
| |
| WI.Layers3DContentView = class Layers3DContentView extends WI.ContentView |
| { |
| constructor() |
| { |
| super(); |
| |
| this.element.classList.add("layers-3d"); |
| |
| this._compositingBordersButtonNavigationItem = new WI.ActivateButtonNavigationItem("layer-borders", WI.UIString("Show compositing borders"), WI.UIString("Hide compositing borders"), "Images/LayerBorders.svg", 13, 13); |
| this._compositingBordersButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCompositingBordersButtonClicked, this); |
| this._compositingBordersButtonNavigationItem.enabled = WI.LayerTreeManager.supportsVisibleCompositingBorders(); |
| this._compositingBordersButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| |
| this._paintFlashingButtonNavigationItem = new WI.ActivateButtonNavigationItem("paint-flashing", WI.UIString("Enable paint flashing"), WI.UIString("Disable paint flashing"), "Images/Paint.svg", 16, 16); |
| this._paintFlashingButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handlePaingFlashingButtonClicked, this); |
| this._paintFlashingButtonNavigationItem.enabled = WI.LayerTreeManager.supportsShowingPaintRects(); |
| this._paintFlashingButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| |
| this._layers = []; |
| this._layerGroupsById = new Map; |
| this._selectedLayerGroup = null; |
| this._candidateSelection = null; |
| this._nodeToSelect = null; |
| |
| this._renderer = null; |
| this._camera = null; |
| this._controls = null; |
| this._scene = null; |
| this._boundingBox = null; |
| this._raycaster = null; |
| this._animationFrameRequestId = null; |
| this._documentNode = null; |
| |
| this._layerInfoElement = null; |
| this._compositedDimensionsElement = null; |
| this._visibleDimensionsElement = null; |
| this._reasonsListElement = null; |
| } |
| |
| // Public |
| |
| get navigationItems() |
| { |
| return [this._compositingBordersButtonNavigationItem, this._paintFlashingButtonNavigationItem]; |
| } |
| |
| get supplementalRepresentedObjects() |
| { |
| return this._layers; |
| } |
| |
| selectLayerById(layerId) |
| { |
| let layerGroup = this._layerGroupsById.get(layerId); |
| this._updateLayerGroupSelection(layerGroup); |
| this._updateLayerInfoElement(); |
| this._centerOnSelection(); |
| } |
| |
| selectLayerForNode(node) |
| { |
| if (!this._layers.length) { |
| this._nodeToSelect = node; |
| return; |
| } |
| |
| this._nodeToSelect = null; |
| |
| let layer = null; |
| while (node && !layer) { |
| layer = this._layers.find((layer) => layer.nodeId === node.id); |
| if (!layer) |
| node = node.parentNode; |
| } |
| |
| console.assert(layer, "There should always be a top level (document) layer"); |
| if (!layer) |
| return; |
| |
| this.selectLayerById(layer.layerId); |
| |
| this.dispatchEventToListeners(WI.Layers3DContentView.Event.SelectedLayerChanged, {layerId: layer.layerId}); |
| } |
| |
| // Protected |
| |
| attached() |
| { |
| super.attached(); |
| |
| WI.layerTreeManager.addEventListener(WI.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this); |
| |
| if (this.didInitialLayout) |
| this._animate(); |
| |
| WI.layerTreeManager.updateCompositingBordersVisibleFromPageIfNeeded(); |
| |
| WI.layerTreeManager.addEventListener(WI.LayerTreeManager.Event.CompositingBordersVisibleChanged, this._handleCompositingBordersVisibleChanged, this); |
| this._handleCompositingBordersVisibleChanged(); |
| |
| WI.layerTreeManager.addEventListener(WI.LayerTreeManager.Event.ShowPaintRectsChanged, this._handleShowPaintRectsChanged, this); |
| this._handleShowPaintRectsChanged(); |
| } |
| |
| detached() |
| { |
| WI.layerTreeManager.removeEventListener(WI.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this); |
| |
| this._stopAnimation(); |
| |
| WI.layerTreeManager.removeEventListener(WI.LayerTreeManager.Event.ShowPaintRectsChanged, this._handleShowPaintRectsChanged, this); |
| WI.layerTreeManager.removeEventListener(WI.LayerTreeManager.Event.CompositingBordersVisibleChanged, this._handleCompositingBordersVisibleChanged, this); |
| |
| super.detached(); |
| } |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this._renderer = new THREE.WebGLRenderer({antialias: true}); |
| this._renderer.setSize(this.element.offsetWidth, this.element.offsetHeight); |
| |
| let updateBackground = () => { |
| this._renderer.setClearColor(window.getComputedStyle(this.element).getPropertyValue("--background-color-content").trim()); |
| }; |
| window.matchMedia("(prefers-color-scheme: dark)").addListener(updateBackground); |
| updateBackground(); |
| |
| this._camera = new THREE.PerspectiveCamera(45, this.element.offsetWidth / this.element.offsetHeight, 1, 100_000); |
| |
| this._controls = new THREE.OrbitControls(this._camera, this._renderer.domElement); |
| this._controls.enableDamping = true; |
| this._controls.panSpeed = 0.5; |
| this._controls.enableKeys = false; |
| this._controls.zoomSpeed = 0.5; |
| this._controls.minDistance = 1000; |
| this._controls.rotateSpeed = 0.5; |
| this._controls.minAzimuthAngle = -Math.PI / 2; |
| this._controls.maxAzimuthAngle = Math.PI / 2; |
| this._controls.screenSpacePanning = true; |
| this._renderer.domElement.addEventListener("contextmenu", (event) => { event.stopPropagation(); }); |
| |
| this._scene = new THREE.Scene; |
| this._boundingBox = new THREE.Box3; |
| |
| this._raycaster = new THREE.Raycaster; |
| this._renderer.domElement.addEventListener("mousedown", this._canvasMouseDown.bind(this)); |
| this._renderer.domElement.addEventListener("mouseup", this._canvasMouseUp.bind(this)); |
| |
| this.element.appendChild(this._renderer.domElement); |
| |
| this._animate(); |
| } |
| |
| layout() |
| { |
| if (this.layoutReason === WI.View.LayoutReason.Resize) |
| return; |
| |
| WI.domManager.requestDocument((node) => { |
| let documentWasUpdated = this._updateDocument(node); |
| |
| WI.layerTreeManager.layersForNode(node, (layers) => { |
| this._updateLayers(layers); |
| |
| if (documentWasUpdated) |
| this._resetCamera(); |
| |
| if (this._nodeToSelect) |
| this.selectLayerForNode(this._nodeToSelect); |
| }); |
| }); |
| } |
| |
| sizeDidChange() |
| { |
| super.sizeDidChange(); |
| |
| this._stopAnimation(); |
| this._camera.aspect = this.element.offsetWidth / this.element.offsetHeight; |
| this._camera.updateProjectionMatrix(); |
| this._renderer.setSize(this.element.offsetWidth, this.element.offsetHeight); |
| this._animate(); |
| } |
| |
| // Private |
| |
| _layerTreeDidChange(event) |
| { |
| this.needsLayout(); |
| } |
| |
| _animate() |
| { |
| this._controls.update(); |
| this._restrictPan(); |
| this._renderer.render(this._scene, this._camera); |
| this._animationFrameRequestId = requestAnimationFrame(() => { this._animate(); }); |
| } |
| |
| _stopAnimation() |
| { |
| cancelAnimationFrame(this._animationFrameRequestId); |
| this._animationFrameRequestId = null; |
| } |
| |
| _updateDocument(documentNode) |
| { |
| if (documentNode === this._documentNode) |
| return false; |
| |
| this._scene.children.length = 0; |
| this._layerGroupsById.clear(); |
| this._layers.length = 0; |
| |
| this._documentNode = documentNode; |
| |
| return true; |
| } |
| |
| _updateLayers(newLayers) |
| { |
| // FIXME: This should be made into the basic usage of the manager, if not the agent itself. |
| // At that point, we can remove this duplication from the visualization and sidebar. |
| let {removals, additions} = WI.layerTreeManager.layerTreeMutations(this._layers, newLayers); |
| |
| for (let layer of removals) { |
| let layerGroup = this._layerGroupsById.get(layer.layerId); |
| this._scene.remove(layerGroup); |
| this._layerGroupsById.delete(layer.layerId); |
| } |
| |
| if (this._selectedLayerGroup && !this._layerGroupsById.get(this._selectedLayerGroup.userData.layer.layerId)) |
| this.selectedLayerGroup = null; |
| |
| for (let layer of additions) { |
| let layerGroup = this._createLayerGroup(layer); |
| this._layerGroupsById.set(layer.layerId, layerGroup); |
| this._scene.add(layerGroup); |
| } |
| |
| // FIXME: Update the backend to provide a literal "layer tree" so we can decide z-indices less naively. |
| const zInterval = 25; |
| newLayers.forEach((layer, index) => { |
| let layerGroup = this._layerGroupsById.get(layer.layerId); |
| layerGroup.position.set(0, 0, index * zInterval); |
| }); |
| |
| this._boundingBox.setFromObject(this._scene); |
| this._controls.maxDistance = this._boundingBox.max.z + WI.Layers3DContentView._zPadding; |
| |
| this._layers = newLayers; |
| this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); |
| } |
| |
| _createLayerGroup(layer) { |
| let layerGroup = new THREE.Group; |
| layerGroup.userData.layer = layer; |
| layerGroup.add(this._createLayerMesh(layer.bounds), this._createLayerMesh(layer.compositedBounds, true)); |
| return layerGroup; |
| } |
| |
| _createLayerMesh({x, y, width, height}, isOutline = false) |
| { |
| let geometry = new THREE.Geometry; |
| geometry.vertices.push( |
| new THREE.Vector3(x, -y, 0), |
| new THREE.Vector3(x, -y - height, 0), |
| new THREE.Vector3(x + width, -y - height, 0), |
| new THREE.Vector3(x + width, -y, 0), |
| ); |
| |
| if (isOutline) { |
| let material = new THREE.LineBasicMaterial({color: WI.Layers3DContentView._layerColor.stroke}); |
| return new THREE.LineLoop(geometry, material); |
| } |
| |
| geometry.faces.push(new THREE.Face3(0, 1, 3), new THREE.Face3(1, 2, 3)); |
| |
| let material = new THREE.MeshBasicMaterial({ |
| color: WI.Layers3DContentView._layerColor.fill, |
| transparent: true, |
| opacity: 0.4, |
| side: THREE.DoubleSide, |
| depthWrite: false, |
| }); |
| |
| return new THREE.Mesh(geometry, material); |
| } |
| |
| _canvasMouseDown(event) |
| { |
| let x = (event.offsetX / event.target.offsetWidth) * 2 - 1; |
| let y = -(event.offsetY / event.target.offsetHeight) * 2 + 1; |
| this._raycaster.setFromCamera(new THREE.Vector2(x, y), this._camera); |
| |
| const recursive = true; |
| let intersects = this._raycaster.intersectObjects(this._scene.children, recursive); |
| let layerGroup = intersects.length ? intersects[0].object.parent : null; |
| this._candidateSelection = {layerGroup}; |
| |
| let canvasMouseMove = (event) => { |
| this._candidateSelection = null; |
| this._renderer.domElement.removeEventListener("mousemove", canvasMouseMove); |
| }; |
| |
| this._renderer.domElement.addEventListener("mousemove", canvasMouseMove); |
| } |
| |
| _canvasMouseUp(event) |
| { |
| if (!this._candidateSelection) |
| return; |
| |
| let selection = this._candidateSelection.layerGroup; |
| if (selection && selection === this._selectedLayerGroup) { |
| if (!event.metaKey) |
| return; |
| |
| selection = null; |
| } |
| |
| this._updateLayerGroupSelection(selection); |
| this._updateLayerInfoElement(); |
| |
| let layerId = selection ? selection.userData.layer.layerId : null; |
| this.dispatchEventToListeners(WI.Layers3DContentView.Event.SelectedLayerChanged, {layerId}); |
| } |
| |
| _updateLayerGroupSelection(layerGroup) |
| { |
| let setColor = ({fill, stroke}) => { |
| let [plane, outline] = this._selectedLayerGroup.children; |
| plane.material.color.set(fill); |
| outline.material.color.set(stroke); |
| }; |
| |
| if (this._selectedLayerGroup) |
| setColor(WI.Layers3DContentView._layerColor); |
| |
| this._selectedLayerGroup = layerGroup; |
| |
| if (this._selectedLayerGroup) |
| setColor(WI.Layers3DContentView._selectedLayerColor); |
| } |
| |
| _centerOnSelection() |
| { |
| if (!this._selectedLayerGroup) |
| return; |
| |
| let {x, y, width, height} = this._selectedLayerGroup.userData.layer.bounds; |
| this._controls.target.set(x + (width / 2), -y - (height / 2), 0); |
| this._camera.position.set(x + (width / 2), -y - (height / 2), this._selectedLayerGroup.position.z + WI.Layers3DContentView._zPadding / 2); |
| } |
| |
| _resetCamera() |
| { |
| let {x, y, width, height} = this._layers[0].bounds; |
| this._controls.target.set(x + (width / 2), -y - (height / 2), 0); |
| this._camera.position.set(x + (width / 2), -y - (height / 2), this._controls.maxDistance - WI.Layers3DContentView._zPadding / 2); |
| } |
| |
| _restrictPan() |
| { |
| let delta = new THREE.Vector3; |
| this._boundingBox.clampPoint(this._controls.target, delta).setZ(0).sub(this._controls.target); |
| this._controls.target.add(delta); |
| this._camera.position.add(delta); |
| } |
| |
| _handleCompositingBordersVisibleChanged(event) |
| { |
| this._compositingBordersButtonNavigationItem.activated = WI.layerTreeManager.compositingBordersVisible; |
| } |
| |
| _handleCompositingBordersButtonClicked(event) |
| { |
| WI.layerTreeManager.compositingBordersVisible = !WI.layerTreeManager.compositingBordersVisible; |
| } |
| |
| _handleShowPaintRectsChanged(event) |
| { |
| this._paintFlashingButtonNavigationItem.activated = WI.layerTreeManager.showPaintRects; |
| } |
| |
| _handlePaingFlashingButtonClicked(event) |
| { |
| WI.layerTreeManager.showPaintRects = !WI.layerTreeManager.showPaintRects; |
| } |
| |
| _buildLayerInfoElement() |
| { |
| this._layerInfoElement = this._element.appendChild(document.createElement("div")); |
| this._layerInfoElement.classList.add("layer-info", "hidden"); |
| |
| let content = this._layerInfoElement.appendChild(document.createElement("div")); |
| content.className = "content"; |
| |
| let dimensionsTitle = content.appendChild(document.createElement("div")); |
| dimensionsTitle.textContent = WI.UIString("Dimensions"); |
| let dimensionsTable = content.appendChild(document.createElement("table")); |
| |
| let compositedRow = dimensionsTable.appendChild(document.createElement("tr")); |
| let compositedLabel = compositedRow.appendChild(document.createElement("td")); |
| compositedLabel.textContent = WI.UIString("Composited"); |
| this._compositedDimensionsElement = compositedRow.appendChild(document.createElement("td")); |
| |
| let visibleRow = dimensionsTable.appendChild(document.createElement("tr")); |
| let visibleLabel = visibleRow.appendChild(document.createElement("td")); |
| visibleLabel.textContent = WI.UIString("Visible"); |
| this._visibleDimensionsElement = visibleRow.appendChild(document.createElement("td")); |
| |
| let reasonsTitle = content.appendChild(document.createElement("div")); |
| reasonsTitle.textContent = WI.UIString("Reasons for compositing"); |
| this._reasonsListElement = content.appendChild(document.createElement("ul")); |
| } |
| |
| _updateLayerInfoElement() |
| { |
| if (!this._layerInfoElement) |
| this._buildLayerInfoElement(); |
| |
| if (!this._selectedLayerGroup) { |
| this._layerInfoElement.classList.add("hidden"); |
| return; |
| } |
| |
| let layer = this._selectedLayerGroup.userData.layer; |
| this._compositedDimensionsElement.textContent = `${layer.compositedBounds.width}px ${multiplicationSign} ${layer.compositedBounds.height}px`; |
| this._visibleDimensionsElement.textContent = `${layer.bounds.width}px ${multiplicationSign} ${layer.bounds.height}px`; |
| |
| WI.layerTreeManager.reasonsForCompositingLayer(layer, (compositingReasons) => { |
| this._updateReasonsList(compositingReasons); |
| this._layerInfoElement.classList.remove("hidden"); |
| }); |
| } |
| |
| _updateReasonsList(compositingReasons) |
| { |
| this._reasonsListElement.removeChildren(); |
| |
| let addReason = (reason) => { |
| let item = this._reasonsListElement.appendChild(document.createElement("li")); |
| item.textContent = reason; |
| }; |
| |
| if (compositingReasons.transform3D) |
| addReason(WI.UIString("Element has a 3D transform")); |
| if (compositingReasons.video) |
| addReason(WI.UIString("Element is <video>")); |
| if (compositingReasons.canvas) |
| addReason(WI.UIString("Element is <canvas>")); |
| if (compositingReasons.plugin) |
| addReason(WI.UIString("Element is a plug-in")); |
| if (compositingReasons.iFrame) |
| addReason(WI.UIString("Element is <iframe>")); |
| if (compositingReasons.model) |
| addReason(WI.UIString("Element is <model>")); |
| if (compositingReasons.backfaceVisibilityHidden) |
| addReason(WI.UIString("Element has \u201Cbackface-visibility: hidden\u201D style")); |
| if (compositingReasons.clipsCompositingDescendants) |
| addReason(WI.UIString("Element clips compositing descendants")); |
| if (compositingReasons.animation) |
| addReason(WI.UIString("Element is animated")); |
| if (compositingReasons.filters) |
| addReason(WI.UIString("Element has CSS filters applied")); |
| if (compositingReasons.positionFixed) |
| addReason(WI.UIString("Element has \u201Cposition: fixed\u201D style")); |
| if (compositingReasons.positionSticky) |
| addReason(WI.UIString("Element has \u201Cposition: sticky\u201D style")); |
| if (compositingReasons.overflowScrollingTouch) |
| addReason(WI.UIString("Element has \u201C-webkit-overflow-scrolling: touch\u201D style")); |
| if (compositingReasons.stacking) |
| addReason(WI.UIString("Element may overlap another compositing element")); |
| if (compositingReasons.overlap) |
| addReason(WI.UIString("Element overlaps other compositing element")); |
| if (compositingReasons.negativeZIndexChildren) |
| addReason(WI.UIString("Element has children with a negative z-index")); |
| if (compositingReasons.transformWithCompositedDescendants) |
| addReason(WI.UIString("Element has a 2D transform and composited descendants")); |
| if (compositingReasons.opacityWithCompositedDescendants) |
| addReason(WI.UIString("Element has opacity applied and composited descendants")); |
| if (compositingReasons.maskWithCompositedDescendants) |
| addReason(WI.UIString("Element is masked and has composited descendants")); |
| if (compositingReasons.reflectionWithCompositedDescendants) |
| addReason(WI.UIString("Element has a reflection and composited descendants")); |
| if (compositingReasons.filterWithCompositedDescendants) |
| addReason(WI.UIString("Element has CSS filters applied and composited descendants")); |
| if (compositingReasons.blendingWithCompositedDescendants) |
| addReason(WI.UIString("Element has CSS blending applied and composited descendants")); |
| if (compositingReasons.isolatesCompositedBlendingDescendants) |
| addReason(WI.UIString("Element is a stacking context and has composited descendants with CSS blending applied")); |
| if (compositingReasons.perspective) |
| addReason(WI.UIString("Element has perspective applied")); |
| if (compositingReasons.preserve3D) |
| addReason(WI.UIString("Element has \u201Ctransform-style: preserve-3d\u201D style")); |
| if (compositingReasons.willChange) |
| addReason(WI.UIString("Element has \u201Cwill-change\u201D style which includes opacity, transform, transform-style, perspective, filter or backdrop-filter")); |
| if (compositingReasons.root) |
| addReason(WI.UIString("Element is the root element")); |
| if (compositingReasons.blending) |
| addReason(WI.UIString("Element has \u201Cblend-mode\u201D style")); |
| } |
| }; |
| |
| WI.Layers3DContentView._zPadding = 3000; |
| |
| WI.Layers3DContentView._layerColor = { |
| fill: "hsl(76, 49%, 75%)", |
| stroke: "hsl(79, 45%, 50%)" |
| }; |
| |
| WI.Layers3DContentView._selectedLayerColor = { |
| fill: "hsl(208, 66%, 79%)", |
| stroke: "hsl(202, 57%, 68%)" |
| }; |
| |
| WI.Layers3DContentView.Event = { |
| SelectedLayerChanged: "selected-layer-changed" |
| }; |