/* * 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}
${stateDiv ?? 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`
Current Backend
${div}
`; } _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`
Hide Inactive Variants
`; } 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`
${stage}  ${vstring}
${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);