/*
* 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': ['essl3', 'essl1'],
'essl1': ['essl3', 'essl1'],
'vulkan': ['glsl', 'spirv'],
'metal': ['msl'],
};
const DEFAULT_LANGUAGE_FOR_BACKEND = {
'opengl': 'essl3',
'essl1': 'essl1',
'vulkan': 'glsl',
'metal': 'msl',
};
const BACKENDS = Object.keys(LANGUAGE_CHOICES);
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 = '#d0d0d0';
const BACKGROUND_COLOR = '#6885c5';
const HOVER_BACKGROUND_COLOR = '#b3c2ff';
const CODE_VIEWER_BOTTOM_ROW_HEIGHT = 60;
const REGULAR_FONT_SIZE = 12;
// 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');`
)}`;
}
};
const _validDict = (obj) => {
return obj && Object.keys(obj).length > 0;
}
const _isMatInfoMode = (database) => {
return Object.keys(database).length == 1;
}
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`
${this.label}
`;
}
}
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,
// Workaround see https://github.com/microsoft/monaco-editor/issues/3217
fontLigatures: '',
});
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));
// It might be that the code is available before the editor has been created.
if (this.code && this.code.length > 0) {
this.editor.setValue(this.code);
}
});
}
_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() {
if (!this.active || !this.modified) {
console.log('Called rebuild while variant is inactive or unmodified');
return;
}
this.dispatchEvent(new CustomEvent(
'rebuild-shader',
{detail: this.editor.getValue(), bubbles: true, composed: true}
));
}
updated(props) {
if (props.has('code') && this.code.length > 0) {
// Note that the prop might have been updated before the editor is available.
if (this.editor) {
this.editor.setValue(this.code);
}
}
if ((props.has('expectedWidth') || props.has('expectedHeight')) &&
(this.expectedWidth > 0 && (this.expectedHeight - CODE_VIEWER_BOTTOM_ROW_HEIGHT) > 0)) {
const actualWidth = Math.floor(this.expectedWidth);
const actualHeight = (Math.floor(this.expectedHeight) - CODE_VIEWER_BOTTOM_ROW_HEIGHT);
this._editorDiv.style.width = actualWidth + 'px';
this._editorDiv.style.height = actualHeight + '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`Please select a shader in the side panel.
`)();
}
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`
${stateText}
`)(): null;
return html`
${reminder ?? nothing}
`;
}
}
customElements.define("code-viewer", CodeViewer);
class MenuSection extends LitElement {
static get properties() {
return {
showing: {type: Boolean, state: true},
title: {type: String, attribute: 'title'},
};
}
static get styles() {
return css`
:host {
font-size: ${unsafeCSS(REGULAR_FONT_SIZE)}px;
color: ${unsafeCSS(UNSELECTED_COLOR)};
}
.section-title {
font-size: 20px;
color: ${unsafeCSS(UNSELECTED_COLOR)};
cursor: pointer;
}
.container {
margin-bottom: 20px;
}
hr {
display: block;
height: 1px;
border: 0px;
border-top: 1px solid ${unsafeCSS(UNSELECTED_COLOR)};
padding: 0;
width: 100%;
margin: 3px 0 8px 0;
}
.expander {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.expanded-title {
color: ${unsafeCSS(FOREGROUND_COLOR)};
}
`;
}
_showClick() {
this.showing = !this.showing;
}
constructor() {
super();
this.showing = true;
}
render() {
const expandedIcon = this.showing ? '-' : '+';
const slot = (() => html``)();
const epandedTitleClass = this.showing ? 'expanded-title' : '';
return html`
${this.title} ${expandedIcon}
${this.showing ? slot : []}
`;
}
}
customElements.define('menu-section', MenuSection);
class MaterialInfo extends LitElement {
static get properties() {
return {
info: {type: Object, state: true},
};
}
static get styles() {
return css`
:host {
font-size: ${unsafeCSS(REGULAR_FONT_SIZE)}px;
}
`;
}
constructor() {
super();
this.info = null;
}
_hasInfo() {
return this.info && Object.keys(this.info).length > 0;
}
render() {
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`
${MATERIAL_INFO_KEY_TO_STRING[propKey]} = ${this.info.shading[propKey]}
`)
);
}
if (this.info.raster) {
infoDivs = infoDivs.concat(
Object.keys(this.info.raster)
.filter((propKey) => (propKey in MATERIAL_INFO_KEY_TO_STRING))
.map((propKey) => html`
${MATERIAL_INFO_KEY_TO_STRING[propKey]} = ${this.info.raster[propKey]}
`)
);
}
}
const shouldHide = infoDivs.length == 0;
if (infoDivs.length > 0) {
return html`
${infoDivs}
`;
}
return html``;
}
}
customElements.define('material-info', MaterialInfo);
class AdvancedOptions extends LitElement {
static get properties() {
return {
currentBackend: {type: String, attribute: 'current-backend'},
currentShaderModel: {type: String, attribute: 'current-shader-model'},
hideInactiveVariants: {type: Boolean, attribute: 'hide-inactive-variants'},
availableBackends: {type: Array, state: true},
};
}
static get styles() {
return css`
:host {
font-size: ${unsafeCSS(REGULAR_FONT_SIZE)}px;
}
.option {
border: 1px solid ${unsafeCSS(UNSELECTED_COLOR)};
border-radius: 5px;
padding: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
margin-bottom: 6px;
}
.borderless {
border: 1px solid rgba(0,0,0,0);
}
label {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
}
label input {
margin: 0 4px 0 0;
}
form {
display: flex;
}
.option-heading {
margin-bottom: 5px;
}
/* Below is a custom checkbox */
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
background-color: var(--form-background);
margin-right: 5px;
font: inherit;
width: 1.15em;
height: 1.15em;
border: 0.15em solid ${unsafeCSS(UNSELECTED_COLOR)};
border-radius: 0.15em;
display: grid;
place-content: center;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
box-shadow: inset 1em 1em var(--form-control-color);
/* Windows High Contrast Mode */
background-color: ${unsafeCSS(FOREGROUND_COLOR)};
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
`;
}
get _backendOptionForm() {
return this.renderRoot.querySelector('#backend-option-form');
}
updated(props) {
if (props.has('currentBackend') || props.has('availableBackends')) {
// Clear the radio button selections. The correct option will be selected
// in _backendOption().
if (this._backendOptionForm) {
this._backendOptionForm.reset();
}
}
}
_backendOption() {
if (this.availableBackends.length == 0) {
return null;
}
const onChange = (ev) => {
const backend = ev.currentTarget.getAttribute('name');
this.dispatchEvent(
new CustomEvent(
'option-backend',
{detail: backend, bubbles: true, composed: true}));
}
const div = this.availableBackends.map((backend) => {
const selected = backend == this.currentBackend;
return html`
`;
});
return html`
`;
}
_hideInactiveVariantsOption() {
if (this.currentShaderModel == 'matinfo') {
return null;
}
const onChange = (ev) => {
this.dispatchEvent(
new CustomEvent(
'option-hide-inactive-variants',
{detail: null, bubbles: true, composed: true}));
}
return html`
`;
}
constructor() {
super();
this.availableBackends = [];
}
render() {
return html`
${this._hideInactiveVariantsOption() ?? nothing}
${this._backendOption() ?? nothing}
`;
}
}
customElements.define('advanced-options', AdvancedOptions);
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 20px 0;
font-size: 25px;
}
.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;
}
`;
}
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'},
hideInactiveVariants: {type: Boolean, attribute: 'hide-inactive-variants'},
currentShaderModel: {type: String, attribute: 'current-shader-model'},
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');
}
get _advancedOptions() {
return this.renderRoot.querySelector('#advanced-options');
}
constructor() {
super();
this.connected = false;
this.materials = [];
this.database = {};
this.activeShaders = {};
this.variants = [];
}
updated(props) {
if (props.has('database')) {
// 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 matids = Object.keys(this.database).sort();
const names = matids.map(matid => (this.database[matid].name || kUntitledPlaceholder));
const labelCount = {};
names.forEach(name => labelCount[name] = (labelCount[name] || 0) + 1);
const duplicates = names.filter(name => labelCount[name] > 1);
this.materials = matids.map((matid) => {
const material = this.database[matid];
let name = material.name || kUntitledPlaceholder;
if (duplicates.includes(name)) {
name = `${name} (${labelCount[name]--})`;
}
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.currentMaterial) {
const material = this.database[this.currentMaterial];
const activeVariants = _validDict(this.activeShaders) ? 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;
// The matinfo usecase
if (_isMatInfoMode(this.database)) {
this._advancedOptions.availableBackends = BACKENDS.filter((backend) => !!material[backend]);
}
}
}
}
_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 && this.currentShaderModel != 'matinfo' ? ' inactive' : '');
const onClickLanguage = this._handleLanguageClick.bind(this, lang);
lang = (isLanguageSelected ? '● ' : '') + lang;
return html`
${lang}
`;
});
return html`${languagesDiv}
`;
}
_buildShaderDiv() {
if (!this.variants) {
return null;
}
const isMatinfo = this.currentShaderModel == 'matinfo';
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;
})
.filter(variant => (!this.hideInactiveVariants || variant.shader.active))
.filter(variant => (
isMatinfo || variant.shader.shaderModel == this.currentShaderModel
))
.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 && !isMatinfo) {
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}]`;
}
const shaderModelSymbol = {
'desktop': '🄳 ',
'mobile': '🄼 ',
};
let languagesDiv = isVariantSelected ? this._buildLanguagesDiv(isActive) : null;
const stage =
(isMatinfo ? shaderModelSymbol[variant.shader.shaderModel] : '') +
(variant.shader.pipelineStage.indexOf('vertex') >=0 ? '𝕍ert' : '𝔽rag');
return html`
${languagesDiv ?? nothing}
`
});
return html`${variants}
`;
}
render() {
const isMatinfo = this.currentShaderModel == 'matinfo';
const sections = (title, domain) => {
const mats = this.materials
.filter((m) => m.domain == domain)
.filter((mat) => {
const material = this.database[mat.matid];
return !this.hideInactiveVariants || material.active;
})
.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';
shaderDiv = this._buildShaderDiv();
}
if (!material.active && !isMatinfo) {
divClass += " inactive";
}
const matName = mat.name;
return html`
${matName}
${shaderDiv ?? nothing}
`;
});
if (mats.length > 0) {
return html`${mats}`;
}
return null;
};
const advancedOptions =
(() => html`
`)();
return html`
${isMatinfo ? 'matinfo' : 'matdbg'}
${sections("Surface", "surface") ?? nothing}
${sections("Post-processing", "postpro") ?? nothing}
${advancedOptions ?? nothing}
`;
}
}
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;
}
);
this.database = await fetchMaterials();
// This is the user preferences stored in localStorage
let hideInactiveVariantsVal = localStorage.getItem('option-hide-inactive-variants');
if (hideInactiveVariantsVal != null) {
this.hideInactiveVariants = hideInactiveVariantsVal == 'true';
}
}
_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.currentShaderModel = null;
this.hideInactiveVariants = false;
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;
}
}
);
this.addEventListener('option-backend',
(ev) => {
this.currentBackend = ev.detail;
}
);
this.addEventListener('option-hide-inactive-variants',
(ev) => {
this.hideInactiveVariants = !this.hideInactiveVariants;
localStorage.setItem('option-hide-inactive-variants', "" + this.hideInactiveVariants);
}
);
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},
currentShaderModel: {type: String, state: true},
codeViewerExpectedWidth: {type: Number, state: true},
codeViewerExpectedHeight: {type: Number, state: true},
hideInactiveVariants: {type: Boolean, state: true},
}
}
updated(props) {
// Set a language if there hasn't been one set.
if (props.has('currentBackend') && this.currentBackend) {
if (LANGUAGE_CHOICES[this.currentBackend].indexOf(this.currentLanguage) < 0) {
this.currentLanguage = DEFAULT_LANGUAGE_FOR_BACKEND[this.currentBackend];
}
}
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) {
const shaderVariant = materialShaders[shader].variant;
if (activeVariants.indexOf(shaderVariant) >= 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 (_validDict(this.activeShaders)) {
let setBackend = null;
let setShaderModel = null;
for (let matid in this.activeShaders) {
const backend = this.activeShaders[matid].backend;
if (!setBackend && backend) {
setBackend = backend;
}
const shaderModel = this.activeShaders[matid].shaderModel;
if (!setShaderModel && shaderModel) {
setShaderModel = shaderModel;
}
}
if (this.currentBackend != setBackend) {
this.currentBackend = setBackend;
}
if (this.currentShaderModel != setShaderModel) {
this.currentShaderModel = setShaderModel;
}
} else if (!this.currentBackend || !this.currentShaderModel) {
// Make a guess on the backend if one wasn't from activeShaders.
this.currentBackend = guessBackend();
this.currentShaderModel = 'matinfo';
}
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;
}
// In the `matinfo -w` usecase, we assume the current material to be the only
// material available in the database.
if (_isMatInfoMode(this.database)) {
this.currentMaterial = Object.keys(this.database)[0];
}
})();
}
}
render() {
const shader = this._getShader();
return html`
`;
}
}
customElements.define("matdbg-viewer", MatdbgViewer);