blob: 57feccc1e7c26559c181d434cfbef8dde277b958 [file] [log] [blame]
<!-- The <wasm-debugger-app-sk> custom element declaration.
The main application element for the Skia Debugger.
Attributes:
None.
Events:
None.
Methods:
None.
-->
<link rel=import href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel=import href="/res/imp/bower_components/iron-icon/iron-icon.html">
<link rel=import href="/res/imp/bower_components/iron-icons/iron-icons.html">
<link rel=import href="/res/imp/bower_components/paper-checkbox/paper-checkbox.html">
<link rel=import href="/res/imp/bower_components/paper-drawer-panel/paper-drawer-panel.html">
<link rel=import href="/res/imp/bower_components/paper-dropdown-menu/paper-dropdown-menu.html">
<link rel=import href="/res/imp/bower_components/paper-header-panel/paper-header-panel.html">
<link rel=import href="/res/imp/bower_components/paper-icon-button/paper-icon-button.html">
<link rel=import href="/res/imp/bower_components/paper-item/paper-item.html">
<link rel=import href="/res/imp/bower_components/paper-listbox/paper-listbox.html">
<link rel=import href="/res/imp/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel=import href="/res/imp/bower_components/paper-radio-group/paper-radio-group.html">
<link rel=import href="/res/imp/bower_components/paper-radio-button/paper-radio-button.html">
<link rel=import href="/res/imp/bower_components/paper-input/paper-input.html">
<link rel=import href="/res/imp/bower_components/paper-toolbar/paper-toolbar.html">
<link rel=import href="/res/imp/bower_components/paper-tabs/paper-tabs.html">
<link rel=import href="/res/imp/bower_components/paper-slider/paper-slider.html">
<link rel=import href="/res/imp/bower_components/iron-pages/iron-pages.html">
<link rel=import href="/res/common/imp/canvas-layers.html">
<link rel=import href="/res/common/imp/crosshair.html">
<link rel=import href="/res/common/imp/dbg-info.html">
<link rel=import href="/res/common/imp/details-summary.html">
<link rel=import href="/res/common/imp/error-toast-sk.html">
<link rel=import href="/res/common/imp/play.html">
<link rel=import href="/res/common/imp/zoom.html">
<dom-module id="wasm-debugger-app-sk">
<style include="iron-flex iron-flex-alignment iron-flex-factors">
:root {
--paper-toolbar-background: #1f78b4;
}
paper-tab {
--paper-tab: {
font-size: 12px;
}
}
paper-tabs {
height: 24px;
}
button {
font-size: 12px;
}
:host {
font-size: 12px;
display: block;
height: 100%
}
#content {
margin: 0.5em;
}
op-sk {
display: block;
}
play-sk {
margin: 5px auto;
}
#center {
overflow: hidden;
}
#rendered {
margin: 0 10px;
overflow: auto;
}
* {
font-family: Helvetica,Arial,'Bitstream Vera Sans',sans-serif;
}
.hidden {
display: none;
}
.shortcuts {
margin-left: 1em;
margin-bottom: 0.5em;
}
.shortcuts td {
padding-left: 0.5em;
}
.shortcuts tr {
padding-bottom: 0.25em;
}
#colorBreakPoint {
margin-left: 3em;
}
#download {
margin: 0 3em;
color: #1f78b4;
}
dbg-info-sk,
.clipcheckbox {
margin-left: 3em;
margin-bottom: 1em;
display: block;
}
paper-radio-button {
margin-left: 3em;
margin-bottom: 1em;
padding: 0;
}
#gpuOpBounds,
paper-toggle-button {
margin: 0.5em 1em;
}
.gpuDrawBoundColor {
color: #E31A1C;
opacity: 0.75;
}
.gpuOpBoundColor {
color: #FF7F00;
opacity: 0.75;
}
.gpuTotalOpColor {
color: #6A3D9A;
opacity: 0.75;
}
paper-tabs {
--paper-tabs-selection-bar-color: #1f78b4;
margin-bottom: 5px;
}
paper-tab {
color: white;
background: gray;
}
paper-tab.iron-selected {
font-weight: bold;
background: #1f78b4;
}
paper-tab[aria-disabled=true] {
color: white;
font-style: italic;
background: lightgray;
}
header {
background: #1f78b4;
color: white;
padding: 0.5em;
margin: 0;
}
header h2,
header div,
header instance-status-sk,
{
display: inline-block;
}
header .hidden {
display: none;
}
header h2 {
margin: 0;
}
dt {
font-weight: bold;
padding: 0.5em 0;
}
dd {
font-family: monospace;
}
.light-checkerboard {
border: solid lightgray 1px;
background-position: 0px 0px, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%),
linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%);
}
.dark-checkerboard {
border: solid lightgray 1px;
background-position: 0px 0px, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(45deg, #555 25%, transparent 25%, transparent 75%, #555 75%, #555 100%),
linear-gradient(45deg, #555 25%, #222 25%, #222 75%, #555 75%, #555 100%);
}
paper-icon-button.resizer
#left paper-icon-button {
margin: 0;
padding: 0;
width: 24px;
height: 24px;
}
#upload summary-sk {
font-weight: bold;
}
#upload div {
padding: 1em 2em;
}
#histogram table {
padding-left: 2em;
}
#histogram .countCol {
text-align: right;
}
#histogram td {
padding: 0.2em;
}
zoom-sk {
display: inline-block;
margin-top: 1em;
box-shadow: 5px 5px 18px grey;
}
#keyboardshortcuts {
margin-top: 1em;
}
#img.bottom {
width: auto;
height: 80vh;
}
#img.fit {
max-width: 100%;
max-height: 80vh;
width: auto;
height: auto;
}
#img.natural {
width: auto;
height: auto;
}
#img.right{
width: 100%;
height: auto;
}
.sizeToolBar paper-icon-button {
opacity: 0.5;
}
.sizeToolBar paper-icon-button:hover {
background-color: #eee;
}
.color-p {
width: 24em;
}
.color-preview {
width: 10px;
height: 10px;
margin: 0;
padding: 0;
display: inline-block;
border: 1px solid black;
}
details-sk {
display: block;
}
#file_input {
width: 100%;
}
.delay-input {
width: 6em;
margin-top: -1em;
}
#frames_slider {
width: 100%;
}
#frame_number {
font-weight: bold;
font-family: sans-serif;
width: 1em;
}
.androidlayerbox {
padding: 5px;
margin-bottom: 5px;
box-shadow: 5px 5px 18px grey;
background-color: #fff;
}
.buttonselected {
background: #1f78b4;
color: white;
}
</style>
<template>
<header class="horizontal layout center">
<h2>Skia WASM Debugger</h2>
<div class=flex></div>
</header>
<div id=content>
<div class="layout horizontal center">
<label>SKP to open:</label>
<input type="file" id="file_input" disabled />
</div>
<div id="multi_frame_controls" class="layout horizontal center" hidden>
<paper-icon-button id="frame_play_button" title="play/pause frames (p)" icon="av:play-circle-outline" on-tap="_toggleFramePlay"></paper-icon-button>
<span id="frame_number">0</span>
<paper-slider id="frames_slider" pin max="10" max-markers="10" step="1" value="{{ frameIndex }}" on-tap="_pauseFrames"></paper-slider>
</div>
<div class="layout horizontal">
<div id=left class$="layout vertical flex-{{ _leftRatio }}">
<div class="layout horizontal end-justified">
<paper-icon-button class=resizer on-tap=_smallerLeft icon="arrow-back" title="Shrink the command panel."></paper-icon-button>
</div>
<div class="layout horizontal">
<paper-input id="fast" on-change="_fastFilter" label="Filter
(Leading ! means remove matches)" placeholder="!save restore"
class=flex></paper-input>
<button id=clear on-tap=_clearFilter>Clear</button>
</div>
<div class="layout horizontal">
<play-sk id=play></play-sk>
<paper-input label="Delay in ms" value="{{ minPlaybackDelay }}" class=delay-input></paper-input>
</div>
<commands-sk id=commands grouping=50></commands-sk>
</div>
<div id=center class="layout vertical flex-3">
<div>
<paper-icon-button class=resizer on-tap=_biggerLeft icon="arrow-forward"
title="Grow the command panel."></paper-icon-button>
</div>
<div id=rendered>
<paper-tabs selected="{{tab_selected}}">
<paper-tab>SKP view</paper-tab>
<paper-tab>Resources</paper-tab>
</paper-tabs>
<iron-pages selected="{{tab_selected}}">
<div>
<div class="layout horizontal sizeToolBar">
<paper-icon-button src="https://debugger-assets.skia.org/res/img/image.png" data-style="natural" title="Original size." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/both.png" data-style="fit" title="Fit to page." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/right.png" data-style="right" title="Fit to page width." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button src="https://debugger-assets.skia.org/res/img/bottom.png" data-style="bottom" title="Fit to height." on-tap="_resizeImage"></paper-icon-button>
<paper-icon-button icon="file-download" title="Save current image" on-tap="_saveImage"></paper-icon-button>
</div>
<canvas-layers-sk layers='["crosshair"]' id=layers on-tap="_crosshairClick" useObservers=false class="light-checkerboard">
<canvas id=img class="fit" width=400 height=400></canvas>
</canvas-layers-sk>
<crosshair-sk id=crosshair target=layers name=crosshair update_on=move hidden></crosshair-sk>
</div>
<div>
<p>{{imageCount}} images were stored in this file. Note that image indices here are file indices, which corresponded 1:1 to the gen ids that the images had during recording. If an image appears more than once, that indicates there were multiple copies of it in memory at record time. These indices appear in the `imageIndex` field of commands using them.</p>
<paper-dropdown-menu label="Image Resources">
<paper-listbox class="dropdown-content" selected="{{selectedResourceImage}}">
<template id="dropdownTemplate" is="dom-repeat" items="[[resouceNameList]]" as="item">
<paper-item value="[[item.value]]">[[item.displayName]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
<br>
<div id="resource" class="light-checkerboard" style="display:inline-block;">
<img id="resource-tab-image"/>
</div>
</div>
</iron-pages>
</div>
</div>
<div id=right class$="layout vertical flex-2">
<div class="layout horizontal">
<paper-toggle-button title="Toggle between Skia making WebGL calls vs. using it's CPU backend and copying the buffer into a Canvas2D element." on-iron-change="_toggleGpuBackend" id=gpu checked="{{render_mode_gpu}}">GPU</paper-toggle-button>
<paper-checkbox disabled="{{!render_mode_gpu}}" checked="{{draw_gpu_op_bounds}}" id=gpuOpBounds on-change="_gpuOpBounds">Display GPU Op Bounds</paper-checkbox>
</div>
<div class="layout horizontal">
<paper-toggle-button title="Show backround as light or dark checkerboard" checked="{{checkerboardMode}}">Light/Dark</paper-toggle-button>
<paper-checkbox checked="{{show_overdraw}}" id=showOverdrawCheckbox on-change="_overdrawHandler">Display Overdraw Vis</paper-checkbox>
</div>
<details-sk open>
<summary-sk>
Bounds and Matrix
</summary-sk>
<paper-checkbox title="Show a semi-transparent teal overlay on the areas within the current clip." id=clip class="clipcheckbox" on-change="_clipHandler">Show Clip</paper-checkbox>
<paper-checkbox title="Show a semi-transparent peach overlay on the areas within the current andorid device clip restriction. This is set at the beginning of each frame and recorded in the DrawAnnotation Command labeled AndroidDeviceClipRestriction" id=androidclip class="clipcheckbox" on-change="_androidClipHandler">Show Android Device Clip Restriction</paper-checkbox>
<dbg-info-sk info="[[ info ]]"></dbg-info-sk>
</details-sk>
<details-sk id=gpuOpBoundsLegend class=hidden>
<summary-sk>
GPU Op Bounds Legend
</summary-sk>
<p style="width: 200px">GPU op bounds are rectangles with a 1 pixel wide stroke. This may mean you can't see them unless you scale the canvas view to its original size.</p>
<table class=shortcuts>
<tr><td class=gpuDrawBoundColor>Bounds for the current draw.</td></tr>
<tr><td class=gpuOpBoundColor>Individual bounds for other draws in the same op.</td></tr>
<tr><td class=gpuTotalOpColor>Total bounds of the current op.</td></tr>
</table>
</details-sk>
<details-sk title="A table of the number of occurrences of each command." id=histogram class="hidden">
<summary-sk>
Histogram
</summary-sk>
<table>
<template is="dom-repeat" items="[[histogram]]">
<tr><td class=countCol>[[item.count]]</td><td>[[item.name]]</td></tr>
</template>
<tr><td class=countCol>[[_filtered.commands.length]]</td><td><b>Total</b></td></tr>
</table>
</details-sk>
<dl>
<dt>Postion</dt>
<dd>([[ x ]], [[ y ]])</dd>
<dt>Color</dt>
<dd><div class=color-preview id=prevColor style="background-color: [[ rgb ]]"></div>[[ rgb ]]</dd>
<dd>[[ hex ]]</dd>
</dl>
<div>
<paper-checkbox title="Pause command playback if the color of the selected pixel changes. To enable, selec a pixel by clicking on the canvas." disabled id=colorBreakPoint on-change="_breakPoint">Break on change.</paper-checkbox>
<div id=colorText class="color-p hidden">
Moving to command <span id=colorTextCommand></span> changed the color of the selected pixel from
<div class=color-preview id=prevColor></div> <span id=colorTextColor1></span> to
<div class=color-preview id=currColor></div> <span id=colorTextColor2></span>.
</div>
</div>
<zoom-sk source=img pixels=21 id=zoom class="light-checkerboard"></zoom-sk>
<details-sk id=keyboardshortcuts class=hidden>
<summary-sk>
Keyboard shortcuts
</summary-sk>
<table class=shortcuts>
<tr><th>H</th><td>Cursor left</td></tr>
<tr><th>L</th><td>Cursor right</td></tr>
<tr><th>J</th><td>Cursor down</td></tr>
<tr><th>K</th><td>Cursor up</td></tr>
<tr><th>.</th><td>Step command forward</td></tr>
<tr><th>,</th><td>Step command back</td></tr>
<tr><th>w</th><td>Previous Frame</td></tr>
<tr><th>s</th><td>Next Frame</td></tr>
<tr><th>p</th><td>Play/Pause frame playback </td></tr>
<tr><td colspan=2>Click the image again to turn off keyboard navigation.</td></tr>
</table>
</details-sk>
<details-sk id=androidlayers open>
<summary-sk>
Layers rendered this frame
</summary-sk>
<template is="dom-repeat" items="[[framelayerlist]]">
<div class="androidlayerbox">
RenderNode Id = <b>[[item.nodeId]]</b>
Command count = <b>[[item.commandCount]]</b><br>
Layer size = <b>([[item.layerWidth]], [[item.layerHeight]])</b>
Full Redraw = <b>[[item.fullRedraw]]</b><br>
<button id=layershowuse on-tap="_ShowLayerUse" title="Cycle through drawImageRectLayer commands on this frame which used this surface as a source.">Show use</button>
<button id=layerinspect on-tap="_InspectLayer" title="Open the SkPicture representing this draw event." class$="[[_InspectorButtonClass(item)]]">[[_InspectorButtonText(item)]]</button>
</div>
</template>
</details-sk>
</div>
</div>
<error-toast-sk></error-toast-sk>
</div>
</template>
</dom-module>
<script>
(function () {
let INDENTERS = {
'Save': { icon: 'icons:save', color: '#B2DF8A', count: 1 },
'SaveLayer': { icon: 'icons:content-copy', color: '#FDBF6F', count: 1 },
'BeginDrawPicture': { icon: 'image:image', color: '#A6CEE3', count: 1 },
};
let OUTDENTERS = ['Restore', 'EndDrawPicture'];
Polymer({
is: 'wasm-debugger-app-sk',
properties: {
tab_selected: {
type: Number,
value: 0,
reflectToAttribute: false,
},
_leftRatio: {
type: Number,
value: 3,
reflectToAttribute: false,
},
render_mode_gpu: {
type: Boolean,
value: true,
reflectToAttribute: false,
},
draw_gpu_op_bounds: {
type: Boolean,
value: false,
reflectToAttribute: false,
},
show_overdraw: {
type: Boolean,
value: false,
reflectToAttribute: false,
},
histogram: {
type: Array,
value: function() { return []; },
reflectToAttribute: false,
},
// this timeout value in ms can control the speed of command or frame playback
// 0 means as fast as possible.
minPlaybackDelay: {
type: Number,
value: 0,
reflectToAttribute: false,
},
frameIndex: {
type: Number,
value: 0,
observer: "_moveFrameTo",
},
imageCount: {
type: Number,
value: 0,
},
selectedResourceImage: {
type: Number,
value: 0,
observer: "_setTabImage",
},
resouceNameList: {
type: Array,
value: [],
},
checkerboardMode: {
type: Boolean,
value: false, // false is light, true is dark
observer: "_checkerboardToggle",
},
framelayerlist: {
type: Array,
value: function() { return []; },
reflectToAttribute: false,
},
inspectedLayer: {
type: Number,
value: -1,
},
},
ready() {
// reference to the active canvas object. used instead of query selector because the query
// selector seemed to produce inconsistent results when replacing canvas during gpu/cpu
// swap. dv stands for debugger view.
this._dvcanvas = document.getElementById('img');
// the instance of SkpDebugPlayer created after loading a file
this._player = null;
// Pointer to the SkSurface being drawn to in the shared wasm memory.
// Can be either a cpu or gpu surface. Passed to every drawTo call.
this._surface = null;
// _targetItem is the index of the op we are in the process of moving to.
// The index is the offset of the op in the this._filtered.commands array.
// When this changes, we've requested to move to this op, but it may not have been
// drawn yet.
this._targetItem = 0;
// The parse, unfiltered JSON command list to be displayed.
this._cmd = {
version: 1,
commands: [],
};
// _filtered contains the .commands that match the current filter, or all
// of the commands if no filter is active.
//
// NB: There is a distintion in the code below between an op's item vs
// index. That is, an ops index never changes, it is the index
// number that the server understands, and is the location of the op
// in this._cmd.commands.
//
// The op item changes, it is the location of the op in
// this._filtered.commands. Some functions use index, some use
// item.
this._filtered = {
version: 1,
commands: [],
};
// Cached json command lists from each frame in a file.
// keys are frame indices.
// The 2nd playthrough of an MSKP is about twice as fast because of this.
this.jsonCommandListCache = {};
// When a multi frame SKP is loaded, this id refers to the timeout id of the _nextFrame()
// call when playback is active. it serves both to indicate the state of frame playback
// and to keep track of the timeout for the purpose of cancelling it.
// command playback makes sense only within a single frame, and is managed by the play-sk
// element. it is automatically paused when the frame is changed.
this.nextFrameTimeout = 0; // currently paused
// When true indicates playback was unpaused while the last command of a frame was selected.
this.drawToEnd = false;
// A map from layer node ids to command indices where they were used this frame, if any.
this.layerUseEvents = {}
// debugger.js (a file compiled by emscripten) defines DebuggerInit.
// It accepts a method to help it locate the .wasm file, and returns a promise
// that resolves when the file has been loaded and the wasm module initialized.
// it provides a reference to the module (called Debugger here).
DebuggerInit({
locateFile: (file) => '/res/'+file,
}).ready().then((Debugger) => {
// Save a reference to the module somewhere we can use it later.
this._debugwasm = Debugger;
// Enable the file input element.
this.$.file_input.disabled = false;
});
this.$.commands.addEventListener('op-selected', (e) => {
// Only force reloading the image if necessary.
let item = this._findItemFromIndex(e.detail.index);
if (this._targetItem !== item) {
this._targetItem = item;
this._moveToTargetItem();
} else {
// We know if we've gotten here that the element wasn't selected by
// a UI action, i.e. we know we're here because we are 'run'ing.
this.$.commands.scrollToTop(e.detail.index);
}
});
this.$.commands.addEventListener('op-toggled', (e) => {
// Toggle the op and the trigger a redraw of the image.
this._player.setCommandVisibility(e.detail.index, e.detail.checked);
this._updateDebuggerView();
});
this.$.commands.addEventListener('op-zoom', (e) => {
this.$.fast.value = e.detail;
this._fastFilter();
});
this.$.commands.addEventListener('op-image-jump', (e) => {
this.tab_selected = 1;
this.selectedResourceImage = e.detail; // this has an observer that handles the rest.
});
// this event would be better named crosshair-moved
this.$.crosshair.addEventListener('crosshair', (e) => {
this.$.zoom.x = e.detail.x;
this.$.zoom.y = e.detail.y;
this.$.zoom.updateZoom();
});
this.$.zoom.addEventListener('zoom-point', (e) => {
this.set('rgb', e.detail.rgb);
this.set('hex', e.detail.hex);
this.set('x', e.detail.x);
this.set('y', e.detail.y);
// Track changes in the color of the selected pixel for stopping packback if breakpoint
// is enabled.
this._prevSelectionColor = this._currSelectionColor
this._currSelectionColor = e.detail.rgb;
});
this.$.zoom.addEventListener('click-to-move', (e) => {
this.$.crosshair.x = e.detail.x;
this.$.crosshair.y = e.detail.y;
this.$.crosshair.coordinatesUpdated();
});
// This event is the play/pause widget telling us to show a certain command.
this.$.play.addEventListener('moveto', (e) => {
if (!this._filtered.commands) {
return;
}
this._targetItem = e.detail.item;
this._moveToTargetItem();
// If moving to this target caused the color of the selected pixel to change, and the
// breakpoint is enabled, pause playback.
if (this.$.colorBreakPoint.checked
&& (this._prevSelectionColor !== this._currSelectionColor)) {
this.$.prevColor.style.backgroundColor = this._prevSelectionColor;
this.$.currColor.style.backgroundColor = this._currSelectionColor;
this.$.colorTextCommand.textContent = this._targetItem;
this.$.colorTextColor1.textContent = this._prevSelectionColor;
this.$.colorTextColor2.textContent = this._currSelectionColor;
this.$.colorText.classList.remove('hidden');
// Stop playback
this.$.play.mode = "pause";
}
});
// This event is the play/pause button being clicked.
this.$.play.addEventListener('mode-changed-manually', (e) => {
// Clear the breakpoint info if the user presses a button.
this.$.colorText.classList.add('hidden');
});
this.$.file_input.addEventListener('change', (e) => {
this._fileInputChanged(e);
});
},
_moveToTargetItem(){
// Constrain to command list length.
this._targetItem = Math.min(this._targetItem, this._filtered.commands.length-1);
// highlight it in the command list.
this.$.commands.item = this._targetItem;
// update wasm module
this._updateDebuggerView();
// Acknowledge we've moved by calling play.movedTo
// save play widget so it can be bound in the closure below.
// call this later, doing it immediately would cause a stack overflow.
// if this were not done, playback would not continue from the selected command
// instead continuing from where the player thinks it is.
setTimeout(() => {
this.$.play.movedTo(this._targetItem);
}, this.minPlaybackDelay);
},
attached() {
document.body.addEventListener('keydown', this._keyDownHandler.bind(this), true);
},
// Called when the filename in the file input element changs
_fileInputChanged(e) {
// Did the change event result in the file-input element specifing a file?
// (user might have cancelled the dialog)
const file = e.target.files[0];
if (!file) {
return;
}
// Create a reader and a callback for when the file finishes being read.
const reader = new FileReader();
reader.onload = (e) => {
// Create the instance of SkpDebugPlayer and load the file.
// This function is provided by helper.js in the JS bundled with the wasm module.
this._openSkpFile(e.target.result);
};
reader.readAsArrayBuffer(file);
},
// Open an SKP or MSKP file. fileContents is expected to be an arraybuffer
// with the file's contents
_openSkpFile(fileContents) {
// Create the instance of SkpDebugPlayer and load the file.
// This function is provided by helper.js in the JS bundled with the wasm module.
this._player = this._debugwasm.SkpFilePlayer(fileContents);
this._replaceSurface();
// Set player clip and overdraw setting to match UI selection, but don't update view yet.
this._clipHandler(false);
this._androidClipHandler(false);
this._overdrawHandler(false);
this._gpuOpBounds();
// Determine if we loaded a single-frame or multi-frame SKP.
if (this._player.getFrameCount() > 1) {
this._pauseFrames();
const frames_count = this._player.getFrameCount();
this.$.frames_slider.max = frames_count;
this.$.frames_slider.max_markers = frames_count;
this.$.frames_slider.value = 0;
this.$.multi_frame_controls.hidden = false;
} else {
this.$.multi_frame_controls.hidden = true;
}
// Request and parse command list for this frame
this.jsonCommandListCache = {};
this._setCommands();
this._moveToLastFilteredCommand();
this.$.zoom.allow_draw = true;
// initialize shared resource viewer
this.imageCount = this._player.getImageCount();
this.set("resouceNameList", this._makeResouceNameList());
window.dispatchEvent(new CustomEvent('partial-resize'));
},
_memoizedJsonCommandList() {
// get the json command list of the player's current frame.
// it costs considerable time to do this, because it's replaying all commands and
// flattening all resources they reference to put them in UrlDataManager.
if (this.frameIndex in this.jsonCommandListCache) {
return this.jsonCommandListCache[this.frameIndex];
}
const json = this._player.jsonCommandList(this._surface);
this.jsonCommandListCache[this.frameIndex] = json;
console.log("json cache miss for frame "+this.frameIndex);
return json;
},
// Called when any of the fit buttons are pressed.
// resises the image of the skp. (the canvas element the wasm is drawing into)
// This resize zooms the canvas without changing the resolution of the surface.
// No refresh is necessary.
_resizeImage(e) {
// what is the purpose of this early return check?
let ele = sk.findParent(e.target, 'PAPER-ICON-BUTTON');
if (!ele) {
return;
}
// Remove any of the 4 fit classes that may be present and apply the requested one.
// The debugger canvas is only meant to have one of these at a time. Each one corresponds
// to one of the fit buttons and sizes the canvas in a different way.
this._dvcanvas.classList.remove('natural', 'fit', 'right', 'bottom');
this._dvcanvas.classList.add(ele.dataset.style);
// this event is received by sk-canvas-layers which resizes it's constituent layers
// including crosshair.
window.dispatchEvent(new CustomEvent('partial-resize'));
this._updateDebuggerView();
},
_saveImage(e) {
// Download the current frame by making an anchor tag with a download attribute.
let a = document.createElement('a');
a.href = this._dvcanvas.toDataURL();
// download attribute becomes the name of the downloaded file.
// use the name of the skp file and the command number to give it a unique name.
let index = this._filtered.commands[this._targetItem]._index;
let mode = (this.render_mode_gpu ? 'gpu' : 'cpu');
a.download = this.$.file_input.files[0].name+'-'+index+'-debug-'+mode+'.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
_smallerLeft() {
if (this._leftRatio > 1) {
this._leftRatio--;
}
},
_biggerLeft() {
if (this._leftRatio < 12) {
this._leftRatio++;
}
},
// Create a new drawing surface. this is called when
// * GPU/CPU mode changes
// * Bounds of the skp change (skp loaded)
// * (not yet supported) Color mode changes
_replaceSurface(e) {
// Discard canvas when switching between cpu/gpu backend because it's bound to a context.
let newCanvas = this._dvcanvas.cloneNode(true);
// Neither parent.replaceChild or parent.removeChild appear to work here more than once
// because they conflict with polymer templates and leave the new node without a parent.
// Thus remove and appendChild are used.
this._dvcanvas.remove();
this._dvcanvas = newCanvas;
this.$.layers.appendChild(newCanvas);
if (this._surface) { this._surface.dispose(); }
if (this._player) {
// From the loaded SKP, player knows how large its picture is. Resize our canvas to match.
let bounds = this._player.getBounds();
this._dvcanvas.width = bounds.fRight - bounds.fLeft;
this._dvcanvas.height = bounds.fBottom - bounds.fTop;
// Still ok to proceed if no skp, the toggle still should work before a file is picked.
}
if (this.render_mode_gpu) {
this._surface = this._debugwasm.MakeWebGLCanvasSurface(this._dvcanvas);
} else {
this._surface = this._debugwasm.MakeSWCanvasSurface(this._dvcanvas);
}
this.$.layers.changeSubject(this._dvcanvas);
this.$.zoom.changeSource(this._dvcanvas);
},
_toggleGpuBackend(e) {
if (!this._debugwasm) { return; }
this._replaceSurface();
this._updateDebuggerView();
},
_isEmpty(s) {
return !s.length;
},
// Called when GPU op bounds checkbox state changes.
_gpuOpBounds(e) {
this._player.setGpuOpBounds(this.draw_gpu_op_bounds);
this._updateDebuggerView();
if (this.draw_gpu_op_bounds) {
this.$.gpuOpBoundsLegend.classList.remove('hidden');
} else {
this.$.gpuOpBoundsLegend.classList.add('hidden');
}
},
_breakPoint() {
if (this.$.colorBreakPoint.checked) {
// We want to be able to jump to any op when the breakpoint
// triggers, so we remove all filtering.
this._clearFilter();
}
},
_findItemFromIndex(index) {
let item = 0;
for (let i = 0; i < this._filtered.commands.length ; i++) {
if (this._filtered.commands[i]._index == index) {
item = i;
break;
}
}
return item;
},
// consider putting these filter functions in the commands element
_clearFilter() {
this.$.fast.value = '';
this._fastFilter();
},
_fastFilter() {
let rawFilter = this.$.fast.value.trim().toLowerCase();
if (rawFilter.indexOf(':') > 0) {
// This is a range filter, e.g. '3:21'.
this._rangeFilter(rawFilter);
} else {
// Text filter, e.g. '!save restore'.
this._textFilter(rawFilter);
}
},
_rangeFilter(rawFilter) {
let parts = rawFilter.split(':');
if (parts.length !== 2) {
sk.errorMessage('Range filters are of the form "N:M".');
return
}
let begin = +parts[0];
let end = +parts[1];
let filtered = {
version: 1,
commands: [],
};
this._cmd.commands.forEach((c, i) => {
if (i >= begin && i <= end) {
filtered.commands.push(c);
}
});
this._setFiltered(filtered);
},
_textFilter(rawFilter) {
let negative = (rawFilter[0] == '!');
if (negative) {
rawFilter = rawFilter.slice(1).trim();
}
let filters = rawFilter.split(/\s+/);
let matches = function(s) {
s = s.toLowerCase();
for (let i = 0; i < filters.length; i++) {
if (negative) {
if (s.indexOf(filters[i]) >= 0) {
return false;
}
} else {
if (s.indexOf(filters[i]) >= 0) {
return true;
}
}
}
return negative;
};
let filtered = {
version: 1,
commands: [],
};
this._cmd.commands.forEach((c) => {
if (matches(JSON.stringify(c.details).toLowerCase())) {
filtered.commands.push(c);
}
});
this._setFiltered(filtered);
},
_setCommands() {
// Cache only holds the regular frame's commands, not layers.
const json = (self.inspectedLayer === -1 ? this._memoizedJsonCommandList()
: this._player.jsonCommandList(this._surface));
this._cmd = this._processCommands(JSON.parse(json));
if (this.$.fast.value) {
this._fastFilter();
} else {
let filtered = {
version: 1,
commands: this._cmd.commands.slice(),
};
this._setFiltered(filtered);
}
this._setLayerList();
},
_setFiltered(filtered) {
this.$.commands.cmd = filtered;
this._filtered = filtered;
this.$.play.size = filtered.commands.length;
},
_moveToLastFilteredCommand() {
this._targetItem = this._filtered.commands.length - 1;
this._moveToTargetItem();
},
_clipHandler(update=true) {
if(this.$.clip.checked) {
// ON: 30% transparent dark teal
this._player.setClipVizColor(parseInt('500e978d',16));
} else {
// OFF: transparent black
this._player.setClipVizColor(0);
}
if (update) {
this._updateDebuggerView();
}
},
_androidClipHandler(update=true) {
this._player.setAndroidClipViz(this.$.androidclip.checked);
if (update) {
this._updateDebuggerView();
}
},
_overdrawHandler(update=false) {
this._player.setOverdrawVis(this.show_overdraw);
if (update) {
this._updateDebuggerView();
}
},
// Asks the wasm module to draw to the provided surface.
// Up to the command index indidated by the command list.
_updateDebuggerView() {
// Get op index from item number using this._filtered.
if (this._filtered.commands.length == 0) {
return;
}
if (this.drawToEnd) {
this._player.draw(this._surface);
} else {
let index = this._filtered.commands[this._targetItem]._index;
this._player.drawTo(this._surface, index);
}
if (!this.render_mode_gpu) {
this._surface.flush();
}
// update zoom
this.$.zoom.updateZoom();
json = this._player.lastCommandInfo();
this.set('info', JSON.parse(json));
},
_crosshairClick(e) {
if (this.$.crosshair.update_on === 'move') {
this.$.crosshair.update_on = 'click';
this.$.colorBreakPoint.disabled = false;
this.$.keyboardshortcuts.classList.remove('hidden');
this.$.crosshair.hidden = false;
} else {
this.$.crosshair.update_on = 'move';
this.$.colorBreakPoint.disabled = true;
this.$.keyboardshortcuts.classList.add('hidden');
this.$.crosshair.hidden = true;
}
},
_keyDownHandler(e) {
if(document.getElementById("fast").focused) {
return; // don't interfere with the filter textbox.
}
let flen = this._filtered.commands.length;
// If adding a case here, document it in the user-visible keyboard shortcuts area.
switch (e.keyCode) {
case 74: // J
this.$.crosshair.y = this.$.crosshair.y+1;
this.$.crosshair.coordinatesUpdated();
break;
case 75: // K
this.$.crosshair.y = this.$.crosshair.y-1;
this.$.crosshair.coordinatesUpdated();
break;
case 72: // H
this.$.crosshair.x = this.$.crosshair.x-1;
this.$.crosshair.coordinatesUpdated();
break;
case 76: // L
this.$.crosshair.x = this.$.crosshair.x+1;
this.$.crosshair.coordinatesUpdated();
break;
case 190: // Period, step command forward
this._targetItem = (flen + this._targetItem + 1) % flen;
this._moveToTargetItem();
break;
case 188: // Comma, step command back
this._targetItem = (flen + this._targetItem - 1) % flen;
this._moveToTargetItem();
break;
case 87: // w
if (this.$.frames_slider.disabled) { return; }
this._moveFrameTo(this.frameIndex-1);
break;
case 83: // s
if (this.$.frames_slider.disabled) { return; }
this._moveFrameTo(this.frameIndex+1);
break;
case 80: // p
this._toggleFramePlay();
break;
default:
return;
}
e.stopPropagation();
},
// Toggle the playback of frames in a multiskp file.
_toggleFramePlay() {
if (this.nextFrameTimeout) {
this._pauseFrames();
} else {
this._playFrames();
}
},
_pauseFrames() {
if (!this.nextFrameTimeout) { return; }
this.$.frame_play_button.icon = "av:play-circle-outline";
clearTimeout(this.nextFrameTimeout);
this.nextFrameTimeout = 0;
this._setCommands();
if (this.drawToEnd) {
this.drawToEnd = false;
this._moveToLastFilteredCommand();
} else {
this._moveToTargetItem();
}
},
_playFrames() {
// Pause command playback if necessary
this.$.play.mode = "pause";
// begin frame playback.
this.$.frame_play_button.icon = "av:pause-circle-outline";
// If the EndDrawPicture command is selected, set a flag causing all frames to draw to
// their final command, until we unpause. that way we don't draw to the command of only
// the shortst frame.
// position in unfiltered command list
if (this._targetItem in this._filtered.commands) {
const position = this._filtered.commands[this._targetItem]._index;
const lenCmds = this._player.getSize(); // number of commands in current frame.
this.drawToEnd = (position === lenCmds - 1);
} else {
this.drawToEnd = true;
}
this._nextFrame();
},
// Advance to the next frame in a multiskp file (or loop back to the beginning)
_nextFrame() {
this._moveFrameTo(this.frameIndex+1);
this.nextFrameTimeout = setTimeout(() => {
this._nextFrame();
// It doesn't really matter that we use minPlaybackDelay for two purposes, since we
// never perform both kinds of playback at the same time.
}, this.minPlaybackDelay);
},
// Move to a specific frame
_moveFrameTo(pos) {
// prevent the recusion that comes from this function being an observer and a mutator of
// this.frameIndex.
if (!this._player || this.supressFrameOberserver) { return };
this.supressFrameOberserver = true;
this.frameIndex = pos;
if (this.frameIndex === -1) {
this.frameIndex = this._player.getFrameCount() - 1;
}
if (this.frameIndex >= this._player.getFrameCount()) {
this.frameIndex = 0;
}
this.$.frame_number.textContent = this.frameIndex;
// Clear the surface back to transparent.
this._surface.getCanvas().clear(this._debugwasm.TRANSPARENT);
this._player.changeFrame(this.frameIndex);
// If the frame moved and the state is paused, also update the command list
if (!this.nextFrameTimeout) {
this._setCommands();
this._moveToTargetItem();
} else {
this._updateDebuggerView();
}
this.supressFrameOberserver = false;
},
_deepCopy(o) {
return JSON.parse(JSON.stringify(o));
},
// _processCommands iterates over the commands to extract several things.
// 1. A depth at every command based on Save/Restore pairs.
// 2. A histogram showing how many times each type of command is used.
// 3. A map from layer node ids to the index of any layer use events in the command list.
_processCommands(cmd) {
let commands = cmd.commands;
let depth = 0;
let prefixes = []; // A stack of indenting commands
let counts = {}; // Tally of each command.
let matchup = []; // Match up saves and restores.
let layeruse = {} // A Map from layer ids to command indices.
for (let i = 0; i < commands.length; i++) {
commands[i] = {
details: commands[i],
_index: i,
};
commands[i]._depth = depth;
commands[i]._prefix = this._deepCopy(prefixes);
let name = commands[i].details.command;
counts[name] = (counts[name] || 0) + 1;
if (name in INDENTERS) {
depth++;
matchup.push(i);
// If this is the same type of indenting op we've already seen
// then just increment the count, otherwise add as a new
// op in prefixes.
if (depth > 1 && prefixes[prefixes.length-1].icon
== INDENTERS[name].icon) {
prefixes[prefixes.length-1].count++;
} else {
prefixes.push(this._deepCopy(INDENTERS[name]));
}
} else if (OUTDENTERS.indexOf(name) !== -1) {
depth--;
// Now that we can match an OUTDENTER with an INDENTER we can set
// the _zoom property for both commands.
let begin = matchup.pop();
let range = begin + ':' + i;
commands[i]._zoom = range;
commands[begin]._zoom = range;
// Only pop the op from prefixes if its count has reached 1.
if (prefixes[prefixes.length-1].count > 1) {
prefixes[prefixes.length-1].count--;
} else {
prefixes.pop();
}
commands[i]._depth = depth;
commands[i]._prefix = this._deepCopy(prefixes);
} else if (name === 'DrawImageRectLayer') {
const node = commands[i].details.layerNodeId;
if (!(node in layeruse)) {
layeruse[node] = [];
}
layeruse[node].push(i);
}
}
this.layerUseEvents = layeruse;
// Calculate the histogram of the ops.
// First convert the object into an Array of objects.
let histogram = [];
for (const k in counts) {
histogram.push({
name: k,
count: counts[k],
});
}
// Now sort the array, descending on the count, ascending
// on the op name.
histogram.sort(function(a,b) {
if (a.count == b.count) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
} else {
return b.count - a.count;
}
});
this.histogram = histogram;
this.$.histogram.classList.remove('hidden');
return cmd;
},
// refresh the layer list from the player any time the frame changes.
_setLayerList() {
let fll = []
const vec = this._player.getLayerDrawEvents();
// this little loop converts from the emscripten binding of a std vector to a js array
for (var i = 0; i < vec.size(); i++) {
fll.push(vec.get(i));
}
this.framelayerlist = fll;
},
_setTabImage(index) {
if (!this._player) { return; }
let pngUri = this._player.getImageResource(index);
document.getElementById("resource-tab-image").src = pngUri
},
_makeResouceNameList() {
let list = [];
for (var i = 0; i < this.imageCount; i++) {
let info = this._player.getImageInfo(i);
list.push({
"value": i,
"displayName": `${i} (${info.width}, ${info.height})`
});
}
return list;
},
_checkerboardToggle() {
let light = "light-checkerboard";
let dark = "dark-checkerboard";
if(this.checkerboardMode) { // dark
this.$.zoom.classList.remove(light);
this.$.layers.classList.remove(light);
this.$.resource.classList.remove(light);
this.$.zoom.classList.add(dark);
this.$.layers.classList.add(dark);
this.$.resource.classList.add(dark);
} else {
this.$.zoom.classList.remove(dark);
this.$.layers.classList.remove(dark);
this.$.resource.classList.remove(dark);
this.$.zoom.classList.add(light);
this.$.layers.classList.add(light);
this.$.resource.classList.add(light);
}
},
_lockFramePlayback(lock) {
this.$.frames_slider.disabled = lock;
this.$.frame_play_button.disabled = lock;
},
_InspectLayer(e, move=true) {
// This method is called any time one of the Inspector/Exit buttons is pressed.
// if the the button was on the layer already being inspected, it says "exit"
// and we should take the user back to the lobby.
if (self.inspectedLayer === e.model.item.nodeId) {
self.inspectedLayer = -1;
this._lockFramePlayback(false);
} else {
// Jump to whichever layer's Inspector button was pushed.
self.inspectedLayer = e.model.item.nodeId;
// TODO(nifong): make it possible to navigate between frames with layer events in new
// timeline widget.
this._lockFramePlayback(true);
}
this._player.setInspectedLayer(self.inspectedLayer);
this._replaceSurface();
this._setCommands();
// if we were asked to move or must move to prevent an error,
if (move || this._targetItem >= this._filtered.commands.length) {
this._moveToLastFilteredCommand();
}
window.dispatchEvent(new CustomEvent('partial-resize'));
},
// functions for computed bindings in the layer list
_InspectorButtonText(item){
return (item.nodeId === self.inspectedLayer ? 'Exit': 'Inspector');
},
_InspectorButtonClass(item){
return (item.nodeId === self.inspectedLayer ? 'buttonselected': '');
},
_ShowLayerUse(e) {
const node = e.model.item.nodeId;
// if this button was clicked while inspecting a layer, jump out of it.
if (self.inspectedLayer === node) {
this._InspectLayer(e, false); // exits layer
}
// It's usual behavior should be to cycle through all uses of this layer on the current
// frame. Use indices are in this.layerUseEvents if any.
if (node in this.layerUseEvents) {
if (this.layerUseLastNodeViewed !== node) {
// looks "show use" was just clicked on a *new* layer, set the cylcer back to 0.
this.layerUseCycler = 0; // this is an index into the this.layerUseEvents[node] list.
}
this.layerUseLastNodeViewed = node;
const indexOfNextUse = this.layerUseEvents[node][this.layerUseCycler];
this.layerUseCycler = (this.layerUseCycler + 1) % this.layerUseEvents[node].length;
const item = this._findItemFromIndex(indexOfNextUse);
if (this._targetItem !== item) {
this._targetItem = item;
this._moveToTargetItem();
}
}
}
}); // end of Polymer(
})(); // end of the declaration of an anonymous function, called.
</script>