feat: 氧化铝数字孪生系统监控大屏完成

This commit is contained in:
2026-04-08 21:44:08 +08:00
commit a48babc68d
67606 changed files with 3337335 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
import { Tab } from '../ui/Tab.js';
class Console extends Tab {
constructor( options = {} ) {
super( 'Console', options );
this.filters = { info: true, warn: true, error: true };
this.filterText = '';
this.buildHeader();
this.logContainer = document.createElement( 'div' );
this.logContainer.id = 'console-log';
this.content.appendChild( this.logContainer );
}
buildHeader() {
const header = document.createElement( 'div' );
header.className = 'console-header';
const filterInput = document.createElement( 'input' );
filterInput.type = 'text';
filterInput.className = 'console-filter-input';
filterInput.placeholder = 'Filter...';
filterInput.addEventListener( 'input', ( e ) => {
this.filterText = e.target.value.toLowerCase();
this.applyFilters();
} );
const copyButton = document.createElement( 'button' );
copyButton.className = 'console-copy-button';
copyButton.title = 'Copy all';
copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
copyButton.addEventListener( 'click', () => this.copyAll( copyButton ) );
const buttonsGroup = document.createElement( 'div' );
buttonsGroup.className = 'console-buttons-group';
Object.keys( this.filters ).forEach( type => {
const label = document.createElement( 'label' );
label.className = 'custom-checkbox';
label.style.color = `var(--${type === 'info' ? 'text-primary' : 'color-' + ( type === 'warn' ? 'yellow' : 'red' )})`;
const checkbox = document.createElement( 'input' );
checkbox.type = 'checkbox';
checkbox.checked = this.filters[ type ];
checkbox.dataset.type = type;
const checkmark = document.createElement( 'span' );
checkmark.className = 'checkmark';
label.appendChild( checkbox );
label.appendChild( checkmark );
label.append( type.charAt( 0 ).toUpperCase() + type.slice( 1 ) );
buttonsGroup.appendChild( label );
} );
buttonsGroup.addEventListener( 'change', ( e ) => {
const type = e.target.dataset.type;
if ( type in this.filters ) {
this.filters[ type ] = e.target.checked;
this.applyFilters();
}
} );
buttonsGroup.appendChild( copyButton );
header.appendChild( filterInput );
header.appendChild( buttonsGroup );
this.content.appendChild( header );
}
applyFilters() {
const messages = this.logContainer.querySelectorAll( '.log-message' );
messages.forEach( msg => {
const type = msg.dataset.type;
const text = msg.dataset.rawText.toLowerCase();
const showByType = this.filters[ type ];
const showByText = text.includes( this.filterText );
msg.classList.toggle( 'hidden', ! ( showByType && showByText ) );
} );
}
copyAll( button ) {
const win = this.logContainer.ownerDocument.defaultView;
const selection = win.getSelection();
const selectedText = selection.toString();
const textInConsole = selectedText && this.logContainer.contains( selection.anchorNode );
let text;
if ( textInConsole ) {
text = selectedText;
} else {
const messages = this.logContainer.querySelectorAll( '.log-message:not(.hidden)' );
text = Array.from( messages ).map( msg => msg.dataset.rawText ).join( '\n' );
}
navigator.clipboard.writeText( text );
button.classList.add( 'copied' );
setTimeout( () => button.classList.remove( 'copied' ), 350 );
}
_getIcon( type, subType ) {
let icon;
if ( subType === 'tip' ) {
icon = '💭';
} else if ( subType === 'tsl' ) {
icon = '✨';
} else if ( subType === 'webgpurenderer' ) {
icon = '🎨';
} else if ( type === 'warn' ) {
icon = '⚠️';
} else if ( type === 'error' ) {
icon = '🔴';
} else if ( type === 'info' ) {
icon = '';
}
return icon;
}
_formatMessage( type, text ) {
const fragment = document.createDocumentFragment();
const prefixMatch = text.match( /^([\w\.]+:\s)/ );
let content = text;
if ( prefixMatch ) {
const fullPrefix = prefixMatch[ 0 ];
const parts = fullPrefix.slice( 0, - 2 ).split( '.' );
const shortPrefix = ( parts.length > 1 ? parts[ parts.length - 1 ] : parts[ 0 ] ) + ':';
const icon = this._getIcon( type, shortPrefix.split( ':' )[ 0 ].toLowerCase() );
fragment.appendChild( document.createTextNode( icon + ' ' ) );
const prefixSpan = document.createElement( 'span' );
prefixSpan.className = 'log-prefix';
prefixSpan.textContent = shortPrefix;
fragment.appendChild( prefixSpan );
content = text.substring( fullPrefix.length );
}
const parts = content.split( /(".*?"|'.*?'|`.*?`)/g ).map( p => p.trim() ).filter( Boolean );
parts.forEach( ( part, index ) => {
if ( /^("|'|`)/.test( part ) ) {
const codeSpan = document.createElement( 'span' );
codeSpan.className = 'log-code';
codeSpan.textContent = part.slice( 1, - 1 );
fragment.appendChild( codeSpan );
} else {
if ( index > 0 ) part = ' ' + part; // add space before parts except the first
if ( index < parts.length - 1 ) part += ' '; // add space between parts
fragment.appendChild( document.createTextNode( part ) );
}
} );
return fragment;
}
addMessage( type, text ) {
const msg = document.createElement( 'div' );
msg.className = `log-message ${type}`;
msg.dataset.type = type;
msg.dataset.rawText = text;
msg.appendChild( this._formatMessage( type, text ) );
const showByType = this.filters[ type ];
const showByText = text.toLowerCase().includes( this.filterText );
msg.classList.toggle( 'hidden', ! ( showByType && showByText ) );
this.logContainer.appendChild( msg );
this.logContainer.scrollTop = this.logContainer.scrollHeight;
if ( this.logContainer.children.length > 200 ) {
this.logContainer.removeChild( this.logContainer.firstChild );
}
}
}
export { Console };

View File

@@ -0,0 +1,348 @@
import { Tab } from '../ui/Tab.js';
import { List } from '../ui/List.js';
import { Item } from '../ui/Item.js';
import { createValueSpan } from '../ui/utils.js';
import { ValueNumber, ValueSlider, ValueSelect, ValueCheckbox, ValueColor, ValueButton } from '../ui/Values.js';
class ParametersGroup {
constructor( parameters, name ) {
this.parameters = parameters;
this.name = name;
this.paramList = new Item( name );
this.objects = [];
}
close() {
this.paramList.close();
return this;
}
add( object, property, ...params ) {
const value = object[ property ];
const type = typeof value;
let item = null;
if ( typeof params[ 0 ] === 'object' ) {
item = this.addSelect( object, property, params[ 0 ] );
} else if ( type === 'number' ) {
if ( params.length >= 2 ) {
item = this.addSlider( object, property, ...params );
} else {
item = this.addNumber( object, property, ...params );
}
} else if ( type === 'boolean' ) {
item = this.addBoolean( object, property );
} else if ( type === 'function' ) {
item = this.addButton( object, property, ...params );
}
return item;
}
_addParameter( object, property, editor, subItem ) {
editor.name = ( name ) => {
subItem.data[ 0 ].textContent = name;
return editor;
};
editor.listen = () => {
const update = () => {
const value = editor.getValue();
const propertyValue = object[ property ];
if ( value !== propertyValue ) {
editor.setValue( propertyValue );
}
requestAnimationFrame( update );
};
requestAnimationFrame( update );
return editor;
};
this._registerParameter( object, property, editor, subItem );
}
_registerParameter( object, property, editor, subItem ) {
this.objects.push( { object: object, key: property, editor: editor, subItem: subItem } );
}
addFolder( name ) {
const group = new ParametersGroup( this.parameters, name );
this.paramList.add( group.paramList );
return group;
}
addBoolean( object, property ) {
const value = object[ property ];
const editor = new ValueCheckbox( { value } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const description = createValueSpan();
description.textContent = property;
const subItem = new Item( description, editor.domElement );
this.paramList.add( subItem );
// extends logic to toggle checkbox when clicking on the row
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
itemRow.addEventListener( 'click', ( e ) => {
if ( e.target.closest( 'label' ) ) return;
const checkbox = itemRow.querySelector( 'input[type="checkbox"]' );
if ( checkbox ) {
checkbox.checked = ! checkbox.checked;
checkbox.dispatchEvent( new Event( 'change' ) );
}
} );
// extend object property
this._addParameter( object, property, editor, subItem );
return editor;
}
addSelect( object, property, options ) {
const value = object[ property ];
const editor = new ValueSelect( { options, value } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const description = createValueSpan();
description.textContent = property;
const subItem = new Item( description, editor.domElement );
this.paramList.add( subItem );
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
// extend object property
this._addParameter( object, property, editor, subItem );
return editor;
}
addColor( object, property ) {
const value = object[ property ];
const editor = new ValueColor( { value } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const description = createValueSpan();
description.textContent = property;
const subItem = new Item( description, editor.domElement );
this.paramList.add( subItem );
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
// extend object property
this._addParameter( object, property, editor, subItem );
return editor;
}
addSlider( object, property, min = 0, max = 1, step = 0.01 ) {
const value = object[ property ];
const editor = new ValueSlider( { value, min, max, step } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const description = createValueSpan();
description.textContent = property;
const subItem = new Item( description, editor.domElement );
this.paramList.add( subItem );
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
// extend object property
this._addParameter( object, property, editor, subItem );
return editor;
}
addNumber( object, property, ...params ) {
const value = object[ property ];
const [ min, max ] = params;
const editor = new ValueNumber( { value, min, max } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const description = createValueSpan();
description.textContent = property;
const subItem = new Item( description, editor.domElement );
this.paramList.add( subItem );
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
// extend object property
this._addParameter( object, property, editor, subItem );
return editor;
}
addButton( object, property ) {
const value = object[ property ];
const editor = new ValueButton( { text: property, value } );
editor.addEventListener( 'change', ( { value } ) => {
object[ property ] = value;
} );
const subItem = new Item( editor.domElement );
subItem.itemRow.childNodes[ 0 ].style.gridColumn = '1 / -1';
this.paramList.add( subItem );
const itemRow = subItem.domElement.firstChild;
itemRow.classList.add( 'actionable' );
// extend object property
editor.name = ( name ) => {
editor.domElement.childNodes[ 0 ].textContent = name;
return editor;
};
this._registerParameter( object, property, editor, subItem );
return editor;
}
}
class Parameters extends Tab {
constructor( options = {} ) {
super( 'Parameters', options );
const paramList = new List( 'Property', 'Value' );
paramList.domElement.classList.add( 'parameters' );
paramList.setGridStyle( '.5fr 1fr' );
paramList.domElement.style.minWidth = '300px';
const scrollWrapper = document.createElement( 'div' );
scrollWrapper.className = 'list-scroll-wrapper';
scrollWrapper.appendChild( paramList.domElement );
this.content.appendChild( scrollWrapper );
this.paramList = paramList;
this.groups = [];
}
createGroup( name ) {
const group = new ParametersGroup( this, name );
this.paramList.add( group.paramList );
this.groups.push( group );
return group;
}
}
export { Parameters };

View File

@@ -0,0 +1,268 @@
import { Tab } from '../ui/Tab.js';
import { List } from '../ui/List.js';
import { Graph } from '../ui/Graph.js';
import { Item } from '../ui/Item.js';
import { createValueSpan, setText } from '../ui/utils.js';
class Performance extends Tab {
constructor( options = {} ) {
super( 'Performance', options );
const perfList = new List( 'Name', 'CPU', 'GPU', 'Total' );
perfList.setGridStyle( 'minmax(200px, 2fr) 80px 80px 80px' );
perfList.domElement.style.minWidth = '600px';
const scrollWrapper = document.createElement( 'div' );
scrollWrapper.className = 'list-scroll-wrapper';
scrollWrapper.appendChild( perfList.domElement );
this.content.appendChild( scrollWrapper );
//
const graphContainer = document.createElement( 'div' );
graphContainer.className = 'graph-container';
const graph = new Graph();
graph.addLine( 'fps', '--accent-color' );
//graph.addLine( 'gpu', '--color-yellow' );
graphContainer.append( graph.domElement );
//
/*
const label = document.createElement( 'label' );
label.className = 'custom-checkbox';
const checkbox = document.createElement( 'input' );
checkbox.type = 'checkbox';
const checkmark = document.createElement( 'span' );
checkmark.className = 'checkmark';
label.appendChild( checkbox );
label.appendChild( checkmark );
*/
const graphStats = new Item( 'Graph Stats', createValueSpan(), createValueSpan(), createValueSpan( 'graph-fps-counter' ) );
perfList.add( graphStats );
const graphItem = new Item( graphContainer );
graphItem.itemRow.childNodes[ 0 ].style.gridColumn = '1 / -1';
graphStats.add( graphItem );
//
const frameStats = new Item( 'Frame Stats', createValueSpan(), createValueSpan(), createValueSpan() );
perfList.add( frameStats );
const miscellaneous = new Item( 'Miscellaneous & Idle', createValueSpan(), createValueSpan(), createValueSpan() );
miscellaneous.domElement.firstChild.style.backgroundColor = '#00ff0b1a';
miscellaneous.domElement.firstChild.classList.add( 'no-hover' );
frameStats.add( miscellaneous );
//
this.notInUse = new Map();
this.frameStats = frameStats;
this.graphStats = graphStats;
this.graph = graph;
this.miscellaneous = miscellaneous;
//
this.currentRender = null;
this.currentItem = null;
this.frameItems = new Map();
}
resolveStats( inspector, stats ) {
const data = inspector.getStatsData( stats.cid );
let item = data.item;
if ( item === undefined ) {
item = new Item( createValueSpan(), createValueSpan(), createValueSpan(), createValueSpan() );
if ( stats.name ) {
if ( stats.isComputeStats === true ) {
stats.name = `${ stats.name } [ Compute ]`;
}
} else {
stats.name = `Unnamed ${ stats.cid }`;
}
item.userData.name = stats.name;
this.currentItem.add( item );
data.item = item;
} else {
item.userData.name = stats.name;
if ( this.notInUse.has( stats.cid ) ) {
item.domElement.firstElementChild.classList.remove( 'alert' );
this.notInUse.delete( stats.cid );
}
const statsIndex = stats.parent.children.indexOf( stats );
if ( item.parent === null || item.parent.children.indexOf( item ) !== statsIndex ) {
this.currentItem.add( item, statsIndex );
}
}
let name = item.userData.name;
if ( stats.isComputeStats ) {
name += ' [ Compute ]';
}
setText( item.data[ 0 ], name );
setText( item.data[ 1 ], data.cpu.toFixed( 2 ) );
setText( item.data[ 2 ], stats.gpuNotAvailable === true ? '-' : data.gpu.toFixed( 2 ) );
setText( item.data[ 3 ], data.total.toFixed( 2 ) );
//
const previousItem = this.currentItem;
this.currentItem = item;
for ( const child of stats.children ) {
this.resolveStats( inspector, child );
}
this.currentItem = previousItem;
this.frameItems.set( stats.cid, item );
}
updateGraph( inspector/*, frame*/ ) {
this.graph.addPoint( 'fps', inspector.fps );
this.graph.update();
}
addNotInUse( cid, item ) {
item.domElement.firstElementChild.classList.add( 'alert' );
this.notInUse.set( cid, {
item,
time: performance.now()
} );
this.updateNotInUse( cid );
}
updateNotInUse( cid ) {
const { item, time } = this.notInUse.get( cid );
const current = performance.now();
const duration = 5;
const remaining = duration - Math.floor( ( current - time ) / 1000 );
if ( remaining >= 0 ) {
const counter = '*'.repeat( Math.max( 0, remaining ) );
const element = item.domElement.querySelector( '.list-item-cell .value' );
setText( element, item.userData.name + ' (not in use) ' + counter );
} else {
item.domElement.firstElementChild.classList.remove( 'alert' );
item.parent.remove( item );
this.notInUse.delete( cid );
}
}
updateText( inspector, frame ) {
const oldFrameItems = new Map( this.frameItems );
this.frameItems.clear();
this.currentItem = this.frameStats;
for ( const child of frame.children ) {
this.resolveStats( inspector, child );
}
// remove unused frame items
for ( const [ cid, item ] of oldFrameItems ) {
if ( ! this.frameItems.has( cid ) ) {
this.addNotInUse( cid, item );
oldFrameItems.delete( cid );
}
}
// update not in use items
for ( const cid of this.notInUse.keys() ) {
this.updateNotInUse( cid );
}
//
setText( 'graph-fps-counter', inspector.fps.toFixed() + ' FPS' );
//
setText( this.frameStats.data[ 1 ], frame.cpu.toFixed( 2 ) );
setText( this.frameStats.data[ 2 ], frame.gpu.toFixed( 2 ) );
setText( this.frameStats.data[ 3 ], frame.total.toFixed( 2 ) );
//
setText( this.miscellaneous.data[ 1 ], frame.miscellaneous.toFixed( 2 ) );
setText( this.miscellaneous.data[ 2 ], '-' );
setText( this.miscellaneous.data[ 3 ], frame.miscellaneous.toFixed( 2 ) );
//
this.currentItem = null;
}
}
export { Performance };

View File

@@ -0,0 +1,166 @@
import { Tab } from '../ui/Tab.js';
import { List } from '../ui/List.js';
import { Item } from '../ui/Item.js';
import { RendererUtils, NoToneMapping, LinearSRGBColorSpace } from 'three/webgpu';
class Viewer extends Tab {
constructor( options = {} ) {
super( 'Viewer', options );
const nodeList = new List( 'Viewer', 'Name' );
nodeList.setGridStyle( '150px minmax(200px, 2fr)' );
nodeList.domElement.style.minWidth = '400px';
const scrollWrapper = document.createElement( 'div' );
scrollWrapper.className = 'list-scroll-wrapper';
scrollWrapper.appendChild( nodeList.domElement );
this.content.appendChild( scrollWrapper );
const nodes = new Item( 'Nodes' );
nodeList.add( nodes );
//
this.itemLibrary = new Map();
this.folderLibrary = new Map();
this.currentDataList = [];
this.nodeList = nodeList;
this.nodes = nodes;
}
getFolder( name ) {
let folder = this.folderLibrary.get( name );
if ( folder === undefined ) {
folder = new Item( name );
this.folderLibrary.set( name, folder );
this.nodeList.add( folder );
}
return folder;
}
addNodeItem( canvasData ) {
let item = this.itemLibrary.get( canvasData.id );
if ( item === undefined ) {
const name = canvasData.name;
const domElement = canvasData.canvasTarget.domElement;
item = new Item( domElement, name );
item.itemRow.children[ 1 ].style[ 'justify-content' ] = 'flex-start';
this.itemLibrary.set( canvasData.id, item );
}
return item;
}
update( renderer, canvasDataList ) {
if ( ! this.isActive && ! this.isDetached ) return;
//
const previousDataList = [ ...this.currentDataList ];
// remove old
for ( const canvasData of previousDataList ) {
if ( this.itemLibrary.has( canvasData.id ) && canvasDataList.indexOf( canvasData ) === - 1 ) {
const item = this.itemLibrary.get( canvasData.id );
const parent = item.parent;
parent.remove( item );
if ( this.folderLibrary.has( parent.data[ 0 ] ) && parent.children.length === 0 ) {
parent.parent.remove( parent );
this.folderLibrary.delete( parent.data[ 0 ] );
}
this.itemLibrary.delete( canvasData.id );
}
}
//
const indexes = {};
for ( const canvasData of canvasDataList ) {
const item = this.addNodeItem( canvasData );
const previousCanvasTarget = renderer.getCanvasTarget();
const path = canvasData.path;
if ( path ) {
const folder = this.getFolder( path );
if ( indexes[ path ] === undefined ) {
indexes[ path ] = 0;
}
if ( folder.parent === null || item.parent !== folder || folder.children.indexOf( item ) !== indexes[ path ] ) {
folder.add( item );
}
indexes[ path ] ++;
} else {
if ( ! item.parent ) {
this.nodes.add( item );
}
}
this.currentDataList = canvasDataList;
//
const state = RendererUtils.resetRendererState( renderer );
renderer.toneMapping = NoToneMapping;
renderer.outputColorSpace = LinearSRGBColorSpace;
renderer.setCanvasTarget( canvasData.canvasTarget );
canvasData.quad.render( renderer );
renderer.setCanvasTarget( previousCanvasTarget );
RendererUtils.restoreRendererState( renderer, state );
}
}
}
export { Viewer };