925 lines
32 KiB
JavaScript
925 lines
32 KiB
JavaScript
/*
|
||
* Copyright (C) 2023 The Android Open Source Project
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
import { LitElement, html, css, unsafeCSS, nothing } from "https://unpkg.com/lit@2.8.0?module";
|
||
|
||
const kUntitledPlaceholder = "untitled";
|
||
|
||
// Maps to backend to the languages allowed for that backend.
|
||
const LANGUAGE_CHOICES = {
|
||
'opengl': ['glsl'],
|
||
'vulkan': ['glsl', 'spirv'],
|
||
'metal': ['msl'],
|
||
};
|
||
|
||
const MATERIAL_INFO_KEY_TO_STRING = {
|
||
'model': 'shading model',
|
||
'vertex_domain': 'vertex domain',
|
||
'interpolation': 'interpolation',
|
||
'shadow_multiply': 'shadow multiply',
|
||
'specular_antialiasing': 'specular antialiasing',
|
||
'variance': 'variance',
|
||
'threshold': 'threshold',
|
||
'clear_coat_IOR_change': 'clear coat IOR change',
|
||
'blending': 'blending',
|
||
'mask_threshold': 'mask threshold',
|
||
'color_write': 'color write',
|
||
'depth_write': 'depth write',
|
||
'depth_test': 'depth test',
|
||
'double_sided': 'double sided',
|
||
'culling': 'culling',
|
||
'transparency': 'transparency',
|
||
};
|
||
|
||
// CSS constants
|
||
const FOREGROUND_COLOR = '#fafafa';
|
||
const INACTIVE_COLOR = '#9a9a9a';
|
||
const DARKER_INACTIVE_COLOR = '#6f6f6f';
|
||
const LIGHTER_INACTIVE_COLOR = '#d9d9d9';
|
||
const UNSELECTED_COLOR = '#dfdfdf';
|
||
const BACKGROUND_COLOR = '#5362e5';
|
||
const HOVER_BACKGROUND_COLOR = '#b3c2ff';
|
||
const CODE_VIEWER_BOTTOM_ROW_HEIGHT = 60;
|
||
const REGULAR_FONT_SIZE = 12;
|
||
const MENU_HR = `
|
||
display: block;
|
||
height: 1px;
|
||
border: 0px;
|
||
border-top: 1px solid ${UNSELECTED_COLOR};
|
||
padding: 0;
|
||
width: 100%;
|
||
margin: 3px 0 8px 0;
|
||
`;
|
||
const MENU_SECTION_TITLE = `
|
||
font-size: 16px;
|
||
color: ${UNSELECTED_COLOR};
|
||
`;
|
||
|
||
// Set up the Monaco editor. See also CodeViewer
|
||
const kMonacoBaseUrl = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.25.2/min/';
|
||
require.config({
|
||
paths: { "vs": `${kMonacoBaseUrl}vs` },
|
||
'vs/css': { disabled: true },
|
||
});
|
||
window.MonacoEnvironment = {
|
||
getWorkerUrl: function() {
|
||
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
||
self.MonacoEnvironment = {
|
||
baseUrl: '${kMonacoBaseUrl}'
|
||
};
|
||
importScripts('${kMonacoBaseUrl}vs/base/worker/workerMain.js');`
|
||
)}`;
|
||
}
|
||
};
|
||
|
||
class Button extends LitElement {
|
||
static get styles() {
|
||
return css`
|
||
:host {
|
||
display: flex;
|
||
}
|
||
.main {
|
||
border: solid 2px ${unsafeCSS(BACKGROUND_COLOR)};
|
||
border-radius: 5px;
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 30px;
|
||
padding: 1px 8px;
|
||
color: ${unsafeCSS(BACKGROUND_COLOR)};
|
||
margin: 5px 10px;
|
||
width: 100px;
|
||
}
|
||
.main:hover {
|
||
background: ${unsafeCSS(HOVER_BACKGROUND_COLOR)};
|
||
}
|
||
.enabled {
|
||
cursor: pointer;
|
||
}
|
||
.disabled:hover {
|
||
background: ${unsafeCSS(LIGHTER_INACTIVE_COLOR)};
|
||
}
|
||
.disabled {
|
||
color: ${unsafeCSS(INACTIVE_COLOR)};
|
||
border: solid 2px ${unsafeCSS(INACTIVE_COLOR)};
|
||
background: ${unsafeCSS(LIGHTER_INACTIVE_COLOR)};
|
||
}
|
||
`;
|
||
}
|
||
static get properties() {
|
||
return {
|
||
label: {type: String, attribute: 'label'},
|
||
enabled: {type: Boolean, attribute: 'enabled'},
|
||
}
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.label = '';
|
||
this.enabled = false;
|
||
}
|
||
|
||
_onClick(ev) {
|
||
this.dispatchEvent(new CustomEvent('button-clicked', {bubbles: true, composed: true}));
|
||
}
|
||
|
||
render() {
|
||
let divClass = 'main';
|
||
if (this.enabled) {
|
||
divClass += ' enabled';
|
||
} else {
|
||
divClass += ' disabled';
|
||
}
|
||
return html`
|
||
<div class="${divClass}" @click="${this._onClick}">
|
||
${this.label}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
customElements.define("custom-button", Button);
|
||
|
||
class CodeViewer extends LitElement {
|
||
static get styles() {
|
||
return css`
|
||
:host {
|
||
background: white;
|
||
width:100%;
|
||
padding-top: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
#editor {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
#bottom-row {
|
||
width: 100%;
|
||
display: flex;
|
||
height: ${unsafeCSS(CODE_VIEWER_BOTTOM_ROW_HEIGHT)}px;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
border-top: solid 1px ${unsafeCSS(BACKGROUND_COLOR)};
|
||
}
|
||
.hide {
|
||
display: none;
|
||
}
|
||
.reminder {
|
||
height: 100%;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
color: ${unsafeCSS(BACKGROUND_COLOR)};
|
||
}
|
||
.stateText {
|
||
color: ${unsafeCSS(INACTIVE_COLOR)};
|
||
padding: 0 10px;
|
||
}
|
||
`;
|
||
}
|
||
|
||
static get properties() {
|
||
return {
|
||
connected: {type: Boolean, attribute: 'connected'},
|
||
code: {type: String, state: true},
|
||
active: {type: Boolean, attribute: 'active'},
|
||
modified: {type: Boolean, attribute: 'modified'},
|
||
expectedWidth: {type: Number, attribute: 'expected-width'},
|
||
expectedHeight: {type: Number, attribute: 'expected-height'},
|
||
}
|
||
}
|
||
|
||
get _editorDiv() {
|
||
return this.renderRoot.querySelector('#editor');
|
||
}
|
||
|
||
firstUpdated() {
|
||
const innerStyle = document.createElement('style');
|
||
innerStyle.innerText = `@import "${kMonacoBaseUrl}/vs/editor/editor.main.css";`;
|
||
this.renderRoot.appendChild(innerStyle);
|
||
|
||
require(["vs/editor/editor.main"], () => {
|
||
this.editor = monaco.editor.create(this._editorDiv, {
|
||
language: "cpp",
|
||
scrollBeyondLastLine: false,
|
||
readOnly: false,
|
||
minimap: { enabled: false },
|
||
automaticLayout: true
|
||
});
|
||
const KeyMod = monaco.KeyMod, KeyCode = monaco.KeyCode;
|
||
this.editor.onDidChangeModelContent(this._onEdit.bind(this));
|
||
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, this._rebuild.bind(this));
|
||
});
|
||
}
|
||
|
||
_onEdit(edit) {
|
||
// If the edit is the loading of the entire code, we ignore the edit.
|
||
if (edit.changes[0].text.length == this.code.length) {
|
||
return;
|
||
}
|
||
this.dispatchEvent(new CustomEvent(
|
||
'shader-edited',
|
||
{detail: this.editor.getValue(), bubbles: true, composed: true}
|
||
));
|
||
}
|
||
|
||
_rebuild() {
|
||
this.dispatchEvent(new CustomEvent(
|
||
'rebuild-shader',
|
||
{detail: this.editor.getValue(), bubbles: true, composed: true}
|
||
));
|
||
}
|
||
|
||
updated(props) {
|
||
if (props.has('code') && this.code.length > 0) {
|
||
this.editor.setValue(this.code);
|
||
}
|
||
if ((props.has('expectedWidth') || props.has('expectedHeight')) &&
|
||
(this.expectedWidth > 0 && (this.expectedHeight - CODE_VIEWER_BOTTOM_ROW_HEIGHT) > 0)) {
|
||
this._editorDiv.style.width = Math.floor(this.expectedWidth) + 'px';
|
||
this._editorDiv.style.height =
|
||
(Math.floor(this.expectedHeight) - CODE_VIEWER_BOTTOM_ROW_HEIGHT) + 'px';
|
||
}
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.code = '';
|
||
this.active = false;
|
||
this.modified = false;
|
||
this.addEventListener('button-clicked', this._rebuild.bind(this));
|
||
this.expectedWidth = 0;
|
||
this.expectedHeight = 0;
|
||
}
|
||
|
||
render() {
|
||
let divClass = '';
|
||
let reminder = null;
|
||
if (this.code.length == 0) {
|
||
divClass += ' hide';
|
||
reminder = (() => html`<div class="reminder">Please select a shader in the side panel.</div>`)();
|
||
}
|
||
let stateText = null;
|
||
if (!this.connected) {
|
||
stateText = 'disconnected';
|
||
} else if (this.code.length > 0 && !this.active) {
|
||
stateText = 'inactive variant/shader';
|
||
} else if (this.code.length > 0 &&!this.modified) {
|
||
stateText = 'source unmodified';
|
||
}
|
||
|
||
const stateDiv = stateText ? (() => html`
|
||
<div class="stateText">${stateText}</div>
|
||
`)(): null;
|
||
|
||
return html`
|
||
<div class="${divClass}" id="editor"></div>
|
||
${reminder ?? nothing}
|
||
<div id="bottom-row">
|
||
<div style="display:flex;flex-direction:row;align-items:center">
|
||
${stateDiv ?? nothing}
|
||
<custom-button class="${divClass}"
|
||
label="Rebuild"
|
||
?enabled="${this.active && this.modified}">
|
||
</custom-button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
customElements.define("code-viewer", CodeViewer);
|
||
|
||
class MaterialInfo extends LitElement {
|
||
static get properties() {
|
||
return {
|
||
info: {type: Object, state: true},
|
||
showing: {type: Boolean, state: true},
|
||
}
|
||
}
|
||
|
||
static get styles() {
|
||
return css`
|
||
:host {
|
||
font-size: ${unsafeCSS(REGULAR_FONT_SIZE)}px;
|
||
color: ${unsafeCSS(UNSELECTED_COLOR)};
|
||
margin-bottom: 20px;
|
||
}
|
||
.section-title {
|
||
${unsafeCSS(MENU_SECTION_TITLE)}
|
||
cursor: pointer;
|
||
}
|
||
hr {
|
||
${unsafeCSS(MENU_HR)}
|
||
}
|
||
.hide {
|
||
display: none;
|
||
flex-direction: column;
|
||
}
|
||
.expander {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
`;
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.showing = true;
|
||
this.info = null;
|
||
}
|
||
|
||
_hasInfo() {
|
||
return this.info && Object.keys(this.info).length > 0;
|
||
}
|
||
|
||
_showClick() {
|
||
this.showing = !this.showing;
|
||
}
|
||
|
||
render() {
|
||
const expandedIcon = this._hasInfo() ? (this.showing ? '-' : '+') : '';
|
||
let infoDivs = [];
|
||
if (this._hasInfo()) {
|
||
if (this.info.shading && this.info.shading.material_domain === 'surface') {
|
||
infoDivs = infoDivs.concat(
|
||
Object.keys(this.info.shading)
|
||
.filter((propKey) => (propKey in MATERIAL_INFO_KEY_TO_STRING))
|
||
.map((propKey) => html`
|
||
<div key="${propKey}" class="info-item">
|
||
${MATERIAL_INFO_KEY_TO_STRING[propKey]} = ${this.info.shading[propKey]}
|
||
</div>
|
||
`)
|
||
);
|
||
}
|
||
if (this.info.raster) {
|
||
infoDivs = infoDivs.concat(
|
||
Object.keys(this.info.raster)
|
||
.filter((propKey) => (propKey in MATERIAL_INFO_KEY_TO_STRING))
|
||
.map((propKey) => html`
|
||
<div key=${propKey} class="info-item">
|
||
${MATERIAL_INFO_KEY_TO_STRING[propKey]} = ${this.info.raster[propKey]}
|
||
</div>
|
||
`)
|
||
);
|
||
}
|
||
}
|
||
let divClass = 'container';
|
||
if (infoDivs.length == 0) {
|
||
divClass += ' hide';
|
||
}
|
||
return html`
|
||
<div class="${divClass}">
|
||
<div class="section-title expander" @click="${this._showClick}">
|
||
<span>Material Details</span> <span>${expandedIcon}</span>
|
||
</div>
|
||
<hr />
|
||
${this.showing ? infoDivs : []}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
customElements.define('material-info', MaterialInfo);
|
||
|
||
class MaterialSidePanel extends LitElement {
|
||
// Setting the style in render() has poor performance implications. We use it simply to avoid
|
||
// having another container descending from the root to host the background color.
|
||
dynamicStyle() {
|
||
return `
|
||
:host {
|
||
background: ${this.connected ? BACKGROUND_COLOR : DARKER_INACTIVE_COLOR};
|
||
width:100%;
|
||
max-width: 250px;
|
||
min-width: 180px;
|
||
padding: 10px 20px;
|
||
overflow-y: auto;
|
||
}
|
||
.title {
|
||
color: white;
|
||
width: 100%;
|
||
text-align: center;
|
||
margin: 0 0 10px 0;
|
||
font-size: 20px;
|
||
}
|
||
.material-section {
|
||
${MENU_SECTION_TITLE}
|
||
}
|
||
.materials {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 20px;
|
||
font-size: ${REGULAR_FONT_SIZE}px;
|
||
color: ${UNSELECTED_COLOR};
|
||
}
|
||
.material_variant_language:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.material_variant_language {
|
||
cursor: pointer;
|
||
}
|
||
.selected {
|
||
font-weight: bolder;
|
||
color: ${FOREGROUND_COLOR};
|
||
}
|
||
.inactive {
|
||
color: ${INACTIVE_COLOR};
|
||
}
|
||
.variant-list {
|
||
padding-left: 20px;
|
||
}
|
||
.language {
|
||
margin: 0 8px 0 0;
|
||
}
|
||
.languages {
|
||
padding-left: 20px;
|
||
flex-direction: row;
|
||
display: flex;
|
||
}
|
||
hr {
|
||
${MENU_HR}
|
||
}
|
||
`;
|
||
}
|
||
|
||
static get properties() {
|
||
return {
|
||
connected: {type: Boolean, attribute: 'connected'},
|
||
currentMaterial: {type: String, attribute: 'current-material'},
|
||
currentShaderIndex: {type: Number, attribute: 'current-shader-index'},
|
||
currentBackend: {type: String, attribute: 'current-backend'},
|
||
currentLanguage: {type: String, attribute: 'current-language'},
|
||
|
||
database: {type: Object, state: true},
|
||
materials: {type: Array, state: true},
|
||
activeShaders: {type: Object, state: true},
|
||
|
||
variants: {type: Array, state: true},
|
||
}
|
||
}
|
||
|
||
get _materialInfo() {
|
||
return this.renderRoot.querySelector('#material-info');
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.connected = false;
|
||
this.materials = [];
|
||
this.database = {};
|
||
this.activeShaders = {};
|
||
this.variants = [];
|
||
}
|
||
|
||
updated(props) {
|
||
if (props.has('database')) {
|
||
const items = [];
|
||
|
||
// Names need not be unique, so we display a numeric suffix for non-unique names.
|
||
// To achieve stable ordering of anonymous materials, we first sort by matid.
|
||
const labels = new Set();
|
||
const matids = Object.keys(this.database).sort();
|
||
const duplicatedLabels = {};
|
||
for (const matid of matids) {
|
||
const name = this.database[matid].name || kUntitledPlaceholder;
|
||
if (labels.has(name)) {
|
||
duplicatedLabels[name] = 0;
|
||
} else {
|
||
labels.add(name);
|
||
}
|
||
}
|
||
|
||
this.materials = matids.map((matid) => {
|
||
const material = this.database[matid];
|
||
let name = material.name || kUntitledPlaceholder;
|
||
if (name in duplicatedLabels) {
|
||
const index = duplicatedLabels[name];
|
||
name = `${name} (${index})`;
|
||
duplicatedLabels[name] = index + 1;
|
||
}
|
||
return {
|
||
matid: matid,
|
||
name: name,
|
||
domain: material.shading.material_domain === "surface" ? "surface" : "postpro",
|
||
active: material.active,
|
||
};
|
||
});
|
||
}
|
||
if (props.has('currentMaterial')) {
|
||
if (this.currentBackend && this.database && this.activeShaders && this.currentMaterial) {
|
||
const material = this.database[this.currentMaterial];
|
||
const activeVariants = this.activeShaders[this.currentMaterial].variants;
|
||
const materialShaders = material[this.currentBackend];
|
||
let variants = [];
|
||
for (const [index, shader] of materialShaders.entries()) {
|
||
const active = activeVariants.indexOf(shader.variant) >= 0;
|
||
variants.push({
|
||
active,
|
||
shader,
|
||
});
|
||
}
|
||
this.variants = variants;
|
||
}
|
||
|
||
if (this.currentMaterial && this.database) {
|
||
const material = this.database[this.currentMaterial];
|
||
this._materialInfo.info = material;
|
||
}
|
||
}
|
||
}
|
||
|
||
_handleMaterialClick(matid, ev) {
|
||
this.dispatchEvent(new CustomEvent('select-material', {detail: matid, bubbles: true, composed: true}));
|
||
}
|
||
|
||
_handleVariantClick(shaderIndex, ev) {
|
||
this.dispatchEvent(new CustomEvent('select-variant', {detail: shaderIndex, bubbles: true, composed: true}));
|
||
}
|
||
|
||
_handleLanguageClick(lang, ev) {
|
||
this.dispatchEvent(new CustomEvent('select-language', {detail: lang, bubbles: true, composed: true}));
|
||
}
|
||
|
||
_buildLanguagesDiv(isActive) {
|
||
const languages = LANGUAGE_CHOICES[this.currentBackend];
|
||
if (!languages || languages.length == 1) {
|
||
return null;
|
||
}
|
||
const languagesDiv = languages.map((lang) => {
|
||
const isLanguageSelected = lang === this.currentLanguage;
|
||
let divClass =
|
||
'material_variant_language language' +
|
||
(isLanguageSelected ? ' selected' : '') +
|
||
(!isActive ? ' inactive' : '');
|
||
const onClickLanguage = this._handleLanguageClick.bind(this, lang);
|
||
lang = (isLanguageSelected ? '● ' : '') + lang;
|
||
return html`
|
||
<div class="${divClass}" @click="${onClickLanguage}">
|
||
${lang}
|
||
</div>
|
||
`;
|
||
});
|
||
return html`<div class="languages">${languagesDiv}</div>`;
|
||
}
|
||
|
||
_buildShaderDiv(showAllShaders) {
|
||
if (!this.variants) {
|
||
return null;
|
||
}
|
||
let variants =
|
||
this.variants
|
||
.sort((a, b) => {
|
||
// Place the active variants up top.
|
||
if (a.active && !b.active) return -1;
|
||
if (b.active && !a.active) return 1;
|
||
return 0;
|
||
})
|
||
.map((variant) => {
|
||
let divClass = 'material_variant_language';
|
||
const shaderIndex = +variant.shader.index;
|
||
const isVariantSelected = this.currentShaderIndex === shaderIndex;
|
||
const isActive = variant.shader.active;
|
||
if (isVariantSelected) {
|
||
divClass += ' selected';
|
||
}
|
||
if (!isActive) {
|
||
divClass += ' inactive';
|
||
}
|
||
const onClickVariant = this._handleVariantClick.bind(this, shaderIndex);
|
||
// Handle the case where variantString is empty (default variant?)
|
||
let vstring = (variant.shader.variantString || '').trim();
|
||
if (vstring.length > 0) {
|
||
vstring = `[${vstring}]`;
|
||
}
|
||
let languagesDiv = isVariantSelected ? this._buildLanguagesDiv(isActive) : null;
|
||
const stage = (isVariantSelected ? '● ' : '') + variant.shader.pipelineStage;
|
||
return html`
|
||
<div class="${divClass}" @click="${onClickVariant}">
|
||
<div>${stage} ${vstring} </div>
|
||
</div>
|
||
${languagesDiv ?? nothing}
|
||
`
|
||
});
|
||
return html`<div class="variant-list">${variants}</div>`;
|
||
}
|
||
|
||
render() {
|
||
const sections = (title, domain) => {
|
||
const mats = this.materials.filter((m) => m.domain == domain).map((mat) => {
|
||
const material = this.database[mat.matid];
|
||
const onClick = this._handleMaterialClick.bind(this, mat.matid);
|
||
let divClass = 'material_variant_language';
|
||
let shaderDiv = null;
|
||
const isMaterialSelected = mat.matid === this.currentMaterial;
|
||
if (isMaterialSelected) {
|
||
divClass += ' selected';
|
||
// If we are looking at an inactive material, show all shaders regardless.
|
||
const showAllShaders = !material.active;
|
||
shaderDiv = this._buildShaderDiv(showAllShaders);
|
||
}
|
||
if (!material.active) {
|
||
divClass += " inactive";
|
||
}
|
||
const matName = (isMaterialSelected ? '● ' : '') + mat.name;
|
||
return html`
|
||
<div class="${divClass}" @click="${onClick}" data-id="${mat.matid}">
|
||
${matName}
|
||
</div>
|
||
${shaderDiv ?? nothing}
|
||
`;
|
||
});
|
||
return html`
|
||
<div class="materials">
|
||
<div class="material-section">
|
||
${title}
|
||
</div>
|
||
<hr />
|
||
${mats}
|
||
</div>
|
||
`;
|
||
};
|
||
return html`
|
||
<style>${this.dynamicStyle()}</style>
|
||
<div class="container">
|
||
<div class="title">matdbg</div>
|
||
${sections("Surface", "surface")}
|
||
${sections("Post-processing", "postpro")}
|
||
<material-info id="material-info"></material-info>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
}
|
||
customElements.define("material-sidepanel", MaterialSidePanel);
|
||
|
||
class MatdbgViewer extends LitElement {
|
||
static get styles() {
|
||
return css`
|
||
:host {
|
||
height: 100%;
|
||
width: 100%;
|
||
display: flex;
|
||
}
|
||
`;
|
||
}
|
||
|
||
get _sidepanel() {
|
||
return this.renderRoot.querySelector('#sidepanel');
|
||
}
|
||
|
||
get _codeviewer() {
|
||
return this.renderRoot.querySelector('#code-viewer');
|
||
}
|
||
|
||
async init() {
|
||
const isConnected = () => this.connected;
|
||
statusLoop(
|
||
isConnected,
|
||
async (status, data) => {
|
||
this.connected = status == STATUS_CONNECTED || status == STATUS_MATERIAL_UPDATED;
|
||
|
||
if (status == STATUS_MATERIAL_UPDATED) {
|
||
let matInfo = await fetchMaterial(matid);
|
||
this.database[matInfo.matid] = matInfo;
|
||
this.database = this.database;
|
||
}
|
||
}
|
||
);
|
||
|
||
activeShadersLoop(
|
||
isConnected,
|
||
(activeShaders) => {
|
||
this.activeShaders = activeShaders;
|
||
}
|
||
);
|
||
|
||
let materials = await fetchMaterials();
|
||
this.database = materials;
|
||
}
|
||
|
||
_getShader() {
|
||
if (!this.currentLanguage || this.currentShaderIndex < 0 || !this.currentBackend) {
|
||
return null;
|
||
}
|
||
const material = (this.database && this.currentMaterial) ? this.database[this.currentMaterial] : null;
|
||
if (!material) {
|
||
return null;
|
||
}
|
||
const shaders = material[this.currentBackend];
|
||
return shaders[this.currentShaderIndex];
|
||
}
|
||
|
||
_onResize() {
|
||
const rect = this._sidepanel.getBoundingClientRect();
|
||
this.codeViewerExpectedWidth = window.innerWidth - rect.width - 1;
|
||
this.codeViewerExpectedHeight = window.innerHeight;
|
||
}
|
||
|
||
firstUpdated() {
|
||
this._onResize();
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.connected = false;
|
||
this.activeShaders = {};
|
||
this.database = {};
|
||
this.currentShaderIndex = -1;
|
||
this.currentMaterial = null;
|
||
this.currentLanguage = null;
|
||
this.currentBackend = null;
|
||
this.init();
|
||
|
||
this.addEventListener('select-material',
|
||
(ev) => {
|
||
this.currentMaterial = ev.detail;
|
||
}
|
||
);
|
||
this.addEventListener('select-variant',
|
||
(ev) => {
|
||
this.currentShaderIndex = ev.detail;
|
||
}
|
||
);
|
||
this.addEventListener('select-language',
|
||
(ev) => {
|
||
this.currentLanguage = ev.detail;
|
||
}
|
||
);
|
||
|
||
this.addEventListener('rebuild-shader',
|
||
(ev) => {
|
||
const shader = this._getShader();
|
||
if (!shader) {
|
||
return
|
||
}
|
||
rebuildMaterial(
|
||
this.currentMaterial, this.currentBackend, this.currentShaderIndex, ev.detail);
|
||
|
||
shader.modified = false;
|
||
// Trigger an update
|
||
this.database = this.database;
|
||
}
|
||
);
|
||
|
||
this.addEventListener('shader-edited',
|
||
(ev) => {
|
||
const shader = this._getShader();
|
||
if (shader) {
|
||
shader.modified = true;
|
||
// Trigger an update
|
||
this.database = this.database;
|
||
}
|
||
}
|
||
);
|
||
|
||
addEventListener('resize', this._onResize.bind(this));
|
||
}
|
||
|
||
static get properties() {
|
||
return {
|
||
connected: {type: Boolean, state: true},
|
||
database: {type: Object, state: true},
|
||
activeShaders: {type: Object, state: true},
|
||
currentLanguage: {type: String, state: true},
|
||
currentMaterial: {type: String, state: true},
|
||
// Each material has a list of variants compiled for it, this index tracks a position in the list.
|
||
currentShaderIndex: {type: Number, state: true},
|
||
currentBackend: {type: String, state: true},
|
||
codeViewerExpectedWidth: {type: Number, state: true},
|
||
codeViewerExpectedHeight: {type: Number, state: true},
|
||
}
|
||
}
|
||
|
||
updated(props) {
|
||
// Set a language if there hasn't been one set.
|
||
if (props.has('currentBackend') && this.currentBackend) {
|
||
const choices = LANGUAGE_CHOICES[this.currentBackend];
|
||
if (choices.indexOf(this.currentLanguage) < 0) {
|
||
this.currentLanguage = choices[0];
|
||
}
|
||
}
|
||
if (props.has('currentMaterial')) {
|
||
// Try to find a default shader index
|
||
if ((this.currentMaterial in this.activeShaders) && this.currentBackend) {
|
||
const material = this.database[this.currentMaterial];
|
||
const activeVariants = this.activeShaders[this.currentMaterial].variants;
|
||
const materialShaders = material[this.currentBackend];
|
||
for (let shader in materialShaders) {
|
||
let ind = activeVariants.indexOf(+shader);
|
||
if (ind >= 0) {
|
||
this.currentShaderIndex = +shader;
|
||
break;
|
||
}
|
||
}
|
||
} else if (this.currentMaterial) {
|
||
const material = this.database[this.currentMaterial];
|
||
// Just pick the first variant in this materials list.
|
||
this.currentShaderIndex = 0;
|
||
}
|
||
}
|
||
if ((props.has('currentMaterial') || props.has('currentBackend') ||
|
||
props.has('currentShaderIndex') || props.has('currentLanguage')) &&
|
||
(this.currentMaterial && this.currentBackend && this.currentShaderIndex >= 0&&
|
||
this.currentLanguage)) {
|
||
(async () => {
|
||
this._codeviewer.code = await fetchShaderCode(
|
||
this.currentMaterial, this.currentBackend, this.currentLanguage,
|
||
this.currentShaderIndex);
|
||
const shader = this._getShader();
|
||
if (shader) {
|
||
shader.modified = false;
|
||
this.database = this.database;
|
||
}
|
||
|
||
// Size of the editor will be adjusted due to the code being loaded, we try to
|
||
// fit the editor again by calling the resize signal.
|
||
setTimeout(this._onResize.bind(this), 700);
|
||
})();
|
||
}
|
||
if (props.has('activeShaders') || props.has('database')) {
|
||
// The only active materials are the ones with active variants.
|
||
Object.values(this.database).forEach((material) => {
|
||
material.active = false;
|
||
});
|
||
for (matid in this.activeShaders) {
|
||
if (!this.database[matid]) {
|
||
continue;
|
||
}
|
||
let material = this.database[matid];
|
||
const backend = this.activeShaders[matid].backend;
|
||
const variants = this.activeShaders[matid].variants;
|
||
for (let shader of material[backend]) {
|
||
shader.active = variants.indexOf(shader.variant) > -1;
|
||
material.active = material.active || shader.active;
|
||
}
|
||
}
|
||
if (this.activeShaders) {
|
||
let backends = {};
|
||
for (let matid in this.activeShaders) {
|
||
const backend = this.activeShaders[matid].backend;
|
||
if (backend in backends) {
|
||
backends[backend] = backends[backend] + 1;
|
||
} else {
|
||
backends[backend] = 1;
|
||
}
|
||
}
|
||
let backendList = Object.keys(backends);
|
||
if (backendList.length > 0) {
|
||
this.currentBackend = backendList[0];
|
||
}
|
||
}
|
||
|
||
this._sidepanel.database = this.database;
|
||
this._sidepanel.activeShaders = this.activeShaders;
|
||
}
|
||
if (props.has('connected') && this.connected) {
|
||
(async () => {
|
||
for (const matId of await fetchMatIds()) {
|
||
const matInfo = await fetchMaterial(matid);
|
||
this.database[matInfo.matid] = matInfo;
|
||
this.database = this.database;
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const shader = this._getShader();
|
||
return html`
|
||
<material-sidepanel id="sidepanel"
|
||
?connected="${this.connected}"
|
||
current-language="${this.currentLanguage}"
|
||
current-shader-index="${this.currentShaderIndex}"
|
||
current-material="${this.currentMaterial}"
|
||
current-backend="${this.currentBackend}" >
|
||
</material-sidepanel>
|
||
<code-viewer id="code-viewer"
|
||
?active=${shader && shader.active}
|
||
?modified=${shader && shader.modified}
|
||
?connected="${this.connected}"
|
||
expected-width="${this.codeViewerExpectedWidth}"
|
||
expected-height="${this.codeViewerExpectedHeight}" >
|
||
</code-viewer>
|
||
`;
|
||
}
|
||
}
|
||
customElements.define("matdbg-viewer", MatdbgViewer);
|