feat: initialize project with core dependencies and game entry point

This commit is contained in:
2026-01-03 01:24:51 -05:00
commit 45d46ddac6
1382 changed files with 844553 additions and 0 deletions

471
node_modules/three/examples/jsm/inspector/Inspector.js generated vendored Normal file
View File

@@ -0,0 +1,471 @@
import { RendererInspector } from './RendererInspector.js';
import { Profiler } from './ui/Profiler.js';
import { Performance } from './tabs/Performance.js';
import { Console } from './tabs/Console.js';
import { Parameters } from './tabs/Parameters.js';
import { Viewer } from './tabs/Viewer.js';
import { setText, splitPath, splitCamelCase } from './ui/utils.js';
import { QuadMesh, NodeMaterial, CanvasTarget, setConsoleFunction, REVISION, NoToneMapping } from 'three/webgpu';
import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform } from 'three/tsl';
const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode ] ) => {
const aspect = uniform( 0 );
OnMaterialUpdate( () => {
const { width, height } = textureNode.value;
aspect.value = width / height;
} );
const centered = uv.sub( 0.5 );
const corrected = vec2( centered.x.div( aspect ), centered.y );
const finalUV = corrected.add( 0.5 );
const inBounds = step( 0.0, finalUV.x ).mul( step( finalUV.x, 1.0 ) ).mul( step( 0.0, finalUV.y ) ).mul( step( finalUV.y, 1.0 ) );
return vec3( finalUV, inBounds );
} );
class Inspector extends RendererInspector {
constructor() {
super();
// init profiler
const profiler = new Profiler();
const parameters = new Parameters( {
builtin: true,
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 6l8 0" /><path d="M16 6l4 0" /><path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 12l2 0" /><path d="M10 12l10 0" /><path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 18l11 0" /><path d="M19 18l1 0" /></svg>'
} );
parameters.hide();
profiler.addTab( parameters );
const viewer = new Viewer();
viewer.hide();
profiler.addTab( viewer );
const performance = new Performance();
profiler.addTab( performance );
const consoleTab = new Console();
profiler.addTab( consoleTab );
profiler.loadLayout();
if ( ! profiler.activeTabId ) {
profiler.setActiveTab( performance.id );
}
this.statsData = new Map();
this.canvasNodes = new Map();
this.profiler = profiler;
this.performance = performance;
this.console = consoleTab;
this.parameters = parameters;
this.viewer = viewer;
this.once = {};
this.displayCycle = {
text: {
needsUpdate: false,
duration: .25,
time: 0
},
graph: {
needsUpdate: false,
duration: .02,
time: 0
}
};
}
get domElement() {
return this.profiler.domElement;
}
resolveConsoleOnce( type, message ) {
const key = type + message;
if ( this.once[ key ] !== true ) {
this.resolveConsole( type, message );
this.once[ key ] = true;
}
}
resolveConsole( type, message ) {
switch ( type ) {
case 'log':
this.console.addMessage( 'info', message );
console.log( message );
break;
case 'warn':
this.console.addMessage( 'warn', message );
console.warn( message );
break;
case 'error':
this.console.addMessage( 'error', message );
console.error( message );
break;
}
}
init() {
const renderer = this.getRenderer();
let sign = `THREE.WebGPURenderer: ${ REVISION } [ "`;
if ( renderer.backend.isWebGPUBackend ) {
sign += 'WebGPU';
} else if ( renderer.backend.isWebGLBackend ) {
sign += 'WebGL2';
}
sign += '" ]';
this.console.addMessage( 'info', sign );
//
if ( renderer.inspector.domElement.parentElement === null && renderer.domElement.parentElement !== null ) {
renderer.domElement.parentElement.appendChild( renderer.inspector.domElement );
}
}
setRenderer( renderer ) {
super.setRenderer( renderer );
if ( renderer !== null ) {
setConsoleFunction( this.resolveConsole.bind( this ) );
if ( this.isAvailable ) {
renderer.backend.trackTimestamp = true;
renderer.init().then( () => {
if ( renderer.hasFeature( 'timestamp-query' ) !== true ) {
this.console.addMessage( 'error', 'THREE.Inspector: GPU Timestamp Queries not available.' );
}
} );
}
}
return this;
}
createParameters( name ) {
if ( this.parameters.isVisible === false ) {
this.parameters.show();
if ( this.parameters.isDetached === false ) {
this.profiler.setActiveTab( this.parameters.id );
}
}
return this.parameters.createGroup( name );
}
getStatsData( cid ) {
let data = this.statsData.get( cid );
if ( data === undefined ) {
data = {};
this.statsData.set( cid, data );
}
return data;
}
resolveStats( stats ) {
const data = this.getStatsData( stats.cid );
if ( data.initialized !== true ) {
data.cpu = stats.cpu;
data.gpu = stats.gpu;
data.stats = [];
data.initialized = true;
}
// store stats
if ( data.stats.length > this.maxFrames ) {
data.stats.shift();
}
data.stats.push( stats );
// compute averages
data.cpu = this.getAverageDeltaTime( data, 'cpu' );
data.gpu = this.getAverageDeltaTime( data, 'gpu' );
data.total = data.cpu + data.gpu;
// children
for ( const child of stats.children ) {
this.resolveStats( child );
const childData = this.getStatsData( child.cid );
data.cpu += childData.cpu;
data.gpu += childData.gpu;
data.total += childData.total;
}
}
getCanvasDataByNode( node ) {
let canvasData = this.canvasNodes.get( node );
if ( canvasData === undefined ) {
const renderer = this.getRenderer();
const canvas = document.createElement( 'canvas' );
const canvasTarget = new CanvasTarget( canvas );
canvasTarget.setPixelRatio( window.devicePixelRatio );
canvasTarget.setSize( 140, 140 );
const id = node.id;
const { path, name } = splitPath( splitCamelCase( node.getName() || '(unnamed)' ) );
const target = node.context( { getUV: ( textureNode ) => {
const uvData = aspectRatioUV( screenUV, textureNode );
const correctedUV = uvData.xy;
const mask = uvData.z;
return correctedUV.mul( mask );
} } );
let output = vec4( vec3( target ), 1 );
output = renderOutput( output, NoToneMapping, renderer.outputColorSpace );
output = output.context( { inspector: true } );
const material = new NodeMaterial();
material.outputNode = output;
const quad = new QuadMesh( material );
quad.name = 'Viewer - ' + name;
canvasData = {
id,
name,
path,
node,
quad,
canvasTarget,
material
};
this.canvasNodes.set( node, canvasData );
}
return canvasData;
}
resolveViewer() {
const nodes = this.currentNodes;
const renderer = this.getRenderer();
if ( nodes.length === 0 ) return;
if ( ! renderer.backend.isWebGPUBackend ) {
this.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' );
return;
}
//
if ( ! this.viewer.isVisible ) {
this.viewer.show();
}
const canvasDataList = nodes.map( node => this.getCanvasDataByNode( node ) );
this.viewer.update( renderer, canvasDataList );
}
getAverageDeltaTime( statsData, property, frames = this.fps ) {
const statsArray = statsData.stats;
let sum = 0;
let count = 0;
for ( let i = statsArray.length - 1; i >= 0 && count < frames; i -- ) {
const stats = statsArray[ i ];
const value = stats[ property ];
if ( value > 0 ) {
// ignore invalid values
sum += value;
count ++;
}
}
return count > 0 ? sum / count : 0;
}
resolveFrame( frame ) {
const nextFrame = this.getFrameById( frame.frameId + 1 );
if ( ! nextFrame ) return;
frame.cpu = 0;
frame.gpu = 0;
frame.total = 0;
for ( const stats of frame.children ) {
this.resolveStats( stats );
const data = this.getStatsData( stats.cid );
frame.cpu += data.cpu;
frame.gpu += data.gpu;
frame.total += data.total;
}
// improve stats using next frame
frame.deltaTime = nextFrame.startTime - frame.startTime;
frame.miscellaneous = frame.deltaTime - frame.total;
if ( frame.miscellaneous < 0 ) {
// Frame desync, probably due to async GPU timing.
frame.miscellaneous = 0;
}
//
this.updateCycle( this.displayCycle.text );
this.updateCycle( this.displayCycle.graph );
if ( this.displayCycle.text.needsUpdate ) {
setText( 'fps-counter', this.fps.toFixed() );
this.performance.updateText( this, frame );
}
if ( this.displayCycle.graph.needsUpdate ) {
this.performance.updateGraph( this, frame );
}
this.displayCycle.text.needsUpdate = false;
this.displayCycle.graph.needsUpdate = false;
}
updateCycle( cycle ) {
cycle.time += this.nodeFrame.deltaTime;
if ( cycle.time >= cycle.duration ) {
cycle.needsUpdate = true;
cycle.time = 0;
}
}
}
export { Inspector };

View File

@@ -0,0 +1,425 @@
import { InspectorBase, TimestampQuery, warnOnce } from 'three/webgpu';
class ObjectStats {
constructor( uid, name ) {
this.uid = uid;
this.cid = uid.match( /^(.*):f(\d+)$/ )[ 1 ]; // call id
this.name = name;
this.timestamp = 0;
this.cpu = 0;
this.gpu = 0;
this.fps = 0;
this.children = [];
this.parent = null;
}
}
class RenderStats extends ObjectStats {
constructor( uid, scene, camera, renderTarget ) {
let name = scene.name;
if ( name === '' ) {
if ( scene.isScene ) {
name = 'Scene';
} else if ( scene.isQuadMesh ) {
name = 'QuadMesh';
}
}
super( uid, name );
this.scene = scene;
this.camera = camera;
this.renderTarget = renderTarget;
this.isRenderStats = true;
}
}
class ComputeStats extends ObjectStats {
constructor( uid, computeNode ) {
super( uid, computeNode.name );
this.computeNode = computeNode;
this.isComputeStats = true;
}
}
export class RendererInspector extends InspectorBase {
constructor() {
super();
this.currentFrame = null;
this.currentRender = null;
this.currentNodes = null;
this.lastFrame = null;
this.frames = [];
this.framesLib = {};
this.maxFrames = 512;
this._lastFinishTime = 0;
this._resolveTimestampPromise = null;
this.isRendererInspector = true;
}
getParent() {
return this.currentRender || this.getFrame();
}
begin() {
this.currentFrame = this._createFrame();
this.currentRender = this.currentFrame;
this.currentNodes = [];
}
finish() {
const now = performance.now();
const frame = this.currentFrame;
frame.finishTime = now;
frame.deltaTime = now - ( this._lastFinishTime > 0 ? this._lastFinishTime : now );
this.addFrame( frame );
this.fps = this._getFPS();
this.lastFrame = frame;
this.currentFrame = null;
this.currentRender = null;
this.currentNodes = null;
this._lastFinishTime = now;
}
_getFPS() {
let frameSum = 0;
let timeSum = 0;
for ( let i = this.frames.length - 1; i >= 0; i -- ) {
const frame = this.frames[ i ];
frameSum ++;
timeSum += frame.deltaTime;
if ( timeSum >= 1000 ) break;
}
return ( frameSum * 1000 ) / timeSum;
}
_createFrame() {
return {
frameId: this.nodeFrame.frameId,
resolvedCompute: false,
resolvedRender: false,
deltaTime: 0,
startTime: performance.now(),
finishTime: 0,
miscellaneous: 0,
children: [],
renders: [],
computes: []
};
}
getFrame() {
return this.currentFrame || this.lastFrame;
}
getFrameById( frameId ) {
return this.framesLib[ frameId ] || null;
}
resolveViewer() { }
resolveFrame( /*frame*/ ) { }
async resolveTimestamp() {
if ( this._resolveTimestampPromise !== null ) {
return this._resolveTimestampPromise;
}
this._resolveTimestampPromise = new Promise( ( resolve ) => {
requestAnimationFrame( async () => {
const renderer = this.getRenderer();
await renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE );
await renderer.resolveTimestampsAsync( TimestampQuery.RENDER );
const computeFrames = renderer.backend.getTimestampFrames( TimestampQuery.COMPUTE );
const renderFrames = renderer.backend.getTimestampFrames( TimestampQuery.RENDER );
const frameIds = [ ...new Set( [ ...computeFrames, ...renderFrames ] ) ];
for ( const frameId of frameIds ) {
const frame = this.getFrameById( frameId );
if ( frame !== null ) {
// resolve compute timestamps
if ( frame.resolvedCompute === false ) {
if ( frame.computes.length > 0 ) {
if ( computeFrames.includes( frameId ) ) {
for ( const stats of frame.computes ) {
if ( renderer.backend.hasTimestamp( stats.uid ) ) {
stats.gpu = renderer.backend.getTimestamp( stats.uid );
} else {
stats.gpu = 0;
stats.gpuNotAvailable = true;
}
}
frame.resolvedCompute = true;
}
} else {
frame.resolvedCompute = true;
}
}
// resolve render timestamps
if ( frame.resolvedRender === false ) {
if ( frame.renders.length > 0 ) {
if ( renderFrames.includes( frameId ) ) {
for ( const stats of frame.renders ) {
if ( renderer.backend.hasTimestamp( stats.uid ) ) {
stats.gpu = renderer.backend.getTimestamp( stats.uid );
} else {
stats.gpu = 0;
stats.gpuNotAvailable = true;
}
}
frame.resolvedRender = true;
}
} else {
frame.resolvedRender = true;
}
}
if ( frame.resolvedCompute === true && frame.resolvedRender === true ) {
this.resolveFrame( frame );
}
}
}
this._resolveTimestampPromise = null;
resolve();
} );
} );
return this._resolveTimestampPromise;
}
get isAvailable() {
const renderer = this.getRenderer();
return renderer !== null;
}
addFrame( frame ) {
// Limit to max frames.
if ( this.frames.length >= this.maxFrames ) {
const removedFrame = this.frames.shift();
delete this.framesLib[ removedFrame.frameId ];
}
this.frames.push( frame );
this.framesLib[ frame.frameId ] = frame;
if ( this.isAvailable ) {
this.resolveViewer();
this.resolveTimestamp();
}
}
inspect( node ) {
const currentNodes = this.currentNodes;
if ( currentNodes !== null ) {
currentNodes.push( node );
} else {
warnOnce( 'RendererInspector: Unable to inspect node outside of frame scope. Use "renderer.setAnimationLoop()".' );
}
}
beginCompute( uid, computeNode ) {
const frame = this.getFrame();
if ( ! frame ) return;
const currentCompute = new ComputeStats( uid, computeNode );
currentCompute.timestamp = performance.now();
currentCompute.parent = this.currentCompute || this.getParent();
frame.computes.push( currentCompute );
if ( this.currentRender !== null ) {
this.currentRender.children.push( currentCompute );
} else {
frame.children.push( currentCompute );
}
this.currentCompute = currentCompute;
}
finishCompute() {
const frame = this.getFrame();
if ( ! frame ) return;
const currentCompute = this.currentCompute;
currentCompute.cpu = performance.now() - currentCompute.timestamp;
this.currentCompute = currentCompute.parent.isComputeStats ? currentCompute.parent : null;
}
beginRender( uid, scene, camera, renderTarget ) {
const frame = this.getFrame();
if ( ! frame ) return;
const currentRender = new RenderStats( uid, scene, camera, renderTarget );
currentRender.timestamp = performance.now();
currentRender.parent = this.getParent();
frame.renders.push( currentRender );
if ( this.currentRender !== null ) {
this.currentRender.children.push( currentRender );
} else {
frame.children.push( currentRender );
}
this.currentRender = currentRender;
}
finishRender() {
const frame = this.getFrame();
if ( ! frame ) return;
const currentRender = this.currentRender;
currentRender.cpu = performance.now() - currentRender.timestamp;
this.currentRender = currentRender.parent;
}
}

View File

@@ -0,0 +1,204 @@
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 filtersGroup = document.createElement( 'div' );
filtersGroup.className = 'console-filters-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 ) );
filtersGroup.appendChild( label );
} );
filtersGroup.addEventListener( 'change', ( e ) => {
const type = e.target.dataset.type;
if ( type in this.filters ) {
this.filters[ type ] = e.target.checked;
this.applyFilters();
}
} );
header.appendChild( filterInput );
header.appendChild( filtersGroup );
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 ) );
} );
}
_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,332 @@
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 );
}
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;
};
}
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;
};
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;
}
createGroup( name ) {
const group = new ParametersGroup( this, name );
this.paramList.add( group.paramList );
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 };

95
node_modules/three/examples/jsm/inspector/ui/Graph.js generated vendored Normal file
View File

@@ -0,0 +1,95 @@
export class Graph {
constructor( maxPoints = 512 ) {
this.maxPoints = maxPoints;
this.lines = {};
this.limit = 0;
this.limitIndex = 0;
this.domElement = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
this.domElement.setAttribute( 'class', 'graph-svg' );
}
addLine( id, color ) {
const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
path.setAttribute( 'class', 'graph-path' );
path.style.stroke = `var(${color})`;
path.style.fill = `var(${color})`;
this.domElement.appendChild( path );
this.lines[ id ] = { path, color, points: [] };
}
addPoint( lineId, value ) {
const line = this.lines[ lineId ];
if ( ! line ) return;
line.points.push( value );
if ( line.points.length > this.maxPoints ) {
line.points.shift();
}
if ( value > this.limit ) {
this.limit = value;
this.limitIndex = 0;
}
}
resetLimit() {
this.limit = 0;
this.limitIndex = 0;
}
update() {
const svgWidth = this.domElement.clientWidth;
const svgHeight = this.domElement.clientHeight;
if ( svgWidth === 0 ) return;
const pointStep = svgWidth / ( this.maxPoints - 1 );
for ( const id in this.lines ) {
const line = this.lines[ id ];
let pathString = `M 0,${ svgHeight }`;
for ( let i = 0; i < line.points.length; i ++ ) {
const x = i * pointStep;
const y = svgHeight - ( line.points[ i ] / this.limit ) * svgHeight;
pathString += ` L ${ x },${ y }`;
}
pathString += ` L ${( line.points.length - 1 ) * pointStep},${ svgHeight } Z`;
const offset = svgWidth - ( ( line.points.length - 1 ) * pointStep );
line.path.setAttribute( 'transform', `translate(${ offset }, 0)` );
line.path.setAttribute( 'd', pathString );
}
//
if ( this.limitIndex ++ > this.maxPoints ) {
this.resetLimit();
}
}
}

170
node_modules/three/examples/jsm/inspector/ui/Item.js generated vendored Normal file
View File

@@ -0,0 +1,170 @@
export class Item {
constructor( ...data ) {
this.children = [];
this.isOpen = true;
this.childrenContainer = null;
this.parent = null;
this.domElement = document.createElement( 'div' );
this.domElement.className = 'list-item-wrapper';
this.itemRow = document.createElement( 'div' );
this.itemRow.className = 'list-item-row';
this.userData = {};
this.data = data;
this.data.forEach( ( cellData ) => {
const cell = document.createElement( 'div' );
cell.className = 'list-item-cell';
if ( cellData instanceof HTMLElement ) {
cell.appendChild( cellData );
} else {
cell.append( String( cellData ) );
}
this.itemRow.appendChild( cell );
} );
this.domElement.appendChild( this.itemRow );
// Bindings
this.onItemClick = this.onItemClick.bind( this );
}
onItemClick( e ) {
if ( e.target.closest( 'button, a, input, label' ) ) return;
this.toggle();
}
add( item, index = this.children.length ) {
if ( item.parent !== null ) {
item.parent.remove( item );
}
item.parent = this;
this.children.splice( index, 0, item );
this.itemRow.classList.add( 'collapsible' );
if ( ! this.childrenContainer ) {
this.childrenContainer = document.createElement( 'div' );
this.childrenContainer.className = 'list-children-container';
this.childrenContainer.classList.toggle( 'closed', ! this.isOpen );
this.domElement.appendChild( this.childrenContainer );
this.itemRow.addEventListener( 'click', this.onItemClick );
}
this.childrenContainer.insertBefore(
item.domElement,
this.childrenContainer.children[ index ] || null
);
this.updateToggler();
return this;
}
remove( item ) {
const index = this.children.indexOf( item );
if ( index !== - 1 ) {
this.children.splice( index, 1 );
this.childrenContainer.removeChild( item.domElement );
item.parent = null;
if ( this.children.length === 0 ) {
this.itemRow.classList.remove( 'collapsible' );
this.itemRow.removeEventListener( 'click', this.onItemClick );
this.childrenContainer.remove();
this.childrenContainer = null;
}
this.updateToggler();
}
return this;
}
updateToggler() {
const firstCell = this.itemRow.querySelector( '.list-item-cell:first-child' );
let toggler = this.itemRow.querySelector( '.item-toggler' );
if ( this.children.length > 0 ) {
if ( ! toggler ) {
toggler = document.createElement( 'span' );
toggler.className = 'item-toggler';
firstCell.prepend( toggler );
}
if ( this.isOpen ) {
this.itemRow.classList.add( 'open' );
}
} else if ( toggler ) {
toggler.remove();
}
}
toggle() {
this.isOpen = ! this.isOpen;
this.itemRow.classList.toggle( 'open', this.isOpen );
if ( this.childrenContainer ) {
this.childrenContainer.classList.toggle( 'closed', ! this.isOpen );
}
return this;
}
close() {
if ( this.isOpen ) {
this.toggle();
}
return this;
}
}

75
node_modules/three/examples/jsm/inspector/ui/List.js generated vendored Normal file
View File

@@ -0,0 +1,75 @@
export class List {
constructor( ...headers ) {
this.headers = headers;
this.children = [];
this.domElement = document.createElement( 'div' );
this.domElement.className = 'list-container';
this.domElement.style.padding = '10px';
this.id = `list-${Math.random().toString( 36 ).substr( 2, 9 )}`;
this.domElement.dataset.listId = this.id;
this.gridStyleElement = document.createElement( 'style' );
this.domElement.appendChild( this.gridStyleElement );
const headerRow = document.createElement( 'div' );
headerRow.className = 'list-header';
this.headers.forEach( headerText => {
const headerCell = document.createElement( 'div' );
headerCell.className = 'list-header-cell';
headerCell.textContent = headerText;
headerRow.appendChild( headerCell );
} );
this.domElement.appendChild( headerRow );
}
setGridStyle( gridTemplate ) {
this.gridStyleElement.textContent = `
[data-list-id="${this.id}"] > .list-header,
[data-list-id="${this.id}"] .list-item-row {
grid-template-columns: ${gridTemplate};
}
`;
}
add( item ) {
if ( item.parent !== null ) {
item.parent.remove( item );
}
item.domElement.classList.add( 'header-wrapper', 'section-start' );
item.parent = this;
this.children.push( item );
this.domElement.appendChild( item.domElement );
}
remove( item ) {
const index = this.children.indexOf( item );
if ( index !== - 1 ) {
this.children.splice( index, 1 );
this.domElement.removeChild( item.domElement );
item.parent = null;
}
return this;
}
}

1975
node_modules/three/examples/jsm/inspector/ui/Profiler.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1589
node_modules/three/examples/jsm/inspector/ui/Style.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

233
node_modules/three/examples/jsm/inspector/ui/Tab.js generated vendored Normal file
View File

@@ -0,0 +1,233 @@
/**
* Tab class
* @param {string} title - The title of the tab
* @param {Object} options - Options for the tab
* @param {boolean} [options.allowDetach=true] - Whether the tab can be detached into a separate window
* @param {boolean} [options.builtin=false] - Whether the tab should appear in the profiler-toggle button
* @param {string} [options.icon] - SVG icon HTML for the builtin button
*
* @example
* // Create a tab that can be detached (default behavior)
* const tab1 = new Tab('My Tab');
*
* // Create a tab that cannot be detached
* const tab2 = new Tab('Fixed Tab', { allowDetach: false });
*
* // Create a builtin tab that appears in the profiler-toggle
* const tab3 = new Tab('Builtin Tab', { builtin: true });
*
* // Create a builtin tab with custom icon
* const tab4 = new Tab('Settings', { builtin: true, icon: '<svg>...</svg>' });
*
* // Control builtin tab visibility
* tab3.showBuiltin(); // Show the builtin button and mini-content
* tab3.hideBuiltin(); // Hide the builtin button and mini-content
*/
export class Tab {
constructor( title, options = {} ) {
this.id = title.toLowerCase();
this.button = document.createElement( 'button' );
this.button.className = 'tab-btn';
this.button.textContent = title;
this.content = document.createElement( 'div' );
this.content.id = `${this.id}-content`;
this.content.className = 'profiler-content';
this.isActive = false;
this.isVisible = true;
this.isDetached = false;
this.detachedWindow = null;
this.allowDetach = options.allowDetach !== undefined ? options.allowDetach : true;
this.builtin = options.builtin !== undefined ? options.builtin : false;
this.icon = options.icon || null;
this.builtinButton = null; // Reference to the builtin button in profiler-toggle
this.miniContent = null; // Reference to the mini-panel content container
this.profiler = null; // Reference to the profiler instance
this.onVisibilityChange = null; // Callback for visibility changes
}
setActive( isActive ) {
this.button.classList.toggle( 'active', isActive );
this.content.classList.toggle( 'active', isActive );
this.isActive = isActive;
}
show() {
this.content.style.display = '';
this.button.style.display = '';
this.isVisible = true;
// Show detached window if tab is detached
if ( this.isDetached && this.detachedWindow ) {
this.detachedWindow.panel.style.display = '';
}
// Notify profiler of visibility change
if ( this.onVisibilityChange ) {
this.onVisibilityChange();
}
this.showBuiltin();
}
hide() {
this.content.style.display = 'none';
this.button.style.display = 'none';
this.isVisible = false;
// Hide detached window if tab is detached
if ( this.isDetached && this.detachedWindow ) {
this.detachedWindow.panel.style.display = 'none';
}
// Notify profiler of visibility change
if ( this.onVisibilityChange ) {
this.onVisibilityChange();
}
this.hideBuiltin();
}
showBuiltin() {
if ( ! this.builtin ) return;
// Show the builtin-tabs-container
if ( this.profiler && this.profiler.builtinTabsContainer ) {
this.profiler.builtinTabsContainer.style.display = '';
}
// Show the button
if ( this.builtinButton ) {
this.builtinButton.style.display = '';
}
// Show and activate the mini-panel with content
if ( this.miniContent && this.profiler ) {
// Hide all other mini-panel contents
this.profiler.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
content.style.display = 'none';
} );
// Remove active state from all builtin buttons
this.profiler.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
btn.classList.remove( 'active' );
} );
// Activate this tab's button
if ( this.builtinButton ) {
this.builtinButton.classList.add( 'active' );
}
// Move content to mini-panel if not already there
if ( ! this.miniContent.firstChild ) {
const actualContent = this.content.querySelector( '.list-scroll-wrapper' ) || this.content.firstElementChild;
if ( actualContent ) {
this.miniContent.appendChild( actualContent );
}
}
// Show the mini-panel and content
this.miniContent.style.display = 'block';
this.profiler.miniPanel.classList.add( 'visible' );
}
}
hideBuiltin() {
if ( ! this.builtin ) return;
// Hide the button
if ( this.builtinButton ) {
this.builtinButton.style.display = 'none';
}
// Hide the mini-panel content
if ( this.miniContent ) {
this.miniContent.style.display = 'none';
// Move content back to main panel
if ( this.miniContent.firstChild ) {
this.content.appendChild( this.miniContent.firstChild );
}
}
// Deactivate button
if ( this.builtinButton ) {
this.builtinButton.classList.remove( 'active' );
}
// Hide mini-panel if no content is visible
if ( this.profiler ) {
const hasVisibleContent = Array.from( this.profiler.miniPanel.querySelectorAll( '.mini-panel-content' ) )
.some( content => content.style.display !== 'none' );
if ( ! hasVisibleContent ) {
this.profiler.miniPanel.classList.remove( 'visible' );
}
// Hide the builtin-tabs-container if all builtin buttons are hidden
const hasVisibleBuiltinButtons = Array.from( this.profiler.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
.some( btn => btn.style.display !== 'none' );
if ( ! hasVisibleBuiltinButtons ) {
this.profiler.builtinTabsContainer.style.display = 'none';
}
}
}
}

439
node_modules/three/examples/jsm/inspector/ui/Values.js generated vendored Normal file
View File

@@ -0,0 +1,439 @@
import { EventDispatcher } from 'three';
class Value extends EventDispatcher {
constructor() {
super();
this.domElement = document.createElement( 'div' );
this.domElement.className = 'param-control';
this._onChangeFunction = null;
this.addEventListener( 'change', ( e ) => {
// defer to avoid issues when changing multiple values in the same call stack
requestAnimationFrame( () => {
if ( this._onChangeFunction ) this._onChangeFunction( e.value );
} );
} );
}
setValue( /*val*/ ) {
this.dispatchChange();
return this;
}
getValue() {
return null;
}
dispatchChange() {
this.dispatchEvent( { type: 'change', value: this.getValue() } );
}
onChange( callback ) {
this._onChangeFunction = callback;
return this;
}
}
class ValueNumber extends Value {
constructor( { value = 0, step = 0.1, min = - Infinity, max = Infinity } ) {
super();
this.input = document.createElement( 'input' );
this.input.type = 'number';
this.input.value = value;
this.input.step = step;
this.input.min = min;
this.input.max = max;
this.input.addEventListener( 'change', this._onChangeValue.bind( this ) );
this.domElement.appendChild( this.input );
this.addDragHandler();
}
_onChangeValue() {
const value = parseFloat( this.input.value );
const min = parseFloat( this.input.min );
const max = parseFloat( this.input.max );
if ( value > max ) {
this.input.value = max;
} else if ( value < min ) {
this.input.value = min;
} else if ( isNaN( value ) ) {
this.input.value = min;
}
this.dispatchChange();
}
addDragHandler() {
let isDragging = false;
let startY, startValue;
this.input.addEventListener( 'mousedown', ( e ) => {
isDragging = true;
startY = e.clientY;
startValue = parseFloat( this.input.value );
document.body.style.cursor = 'ns-resize';
} );
document.addEventListener( 'mousemove', ( e ) => {
if ( isDragging ) {
const deltaY = startY - e.clientY;
const step = parseFloat( this.input.step ) || 1;
const min = parseFloat( this.input.min );
const max = parseFloat( this.input.max );
let stepSize = step;
if ( ! isNaN( max ) && isFinite( min ) ) {
stepSize = ( max - min ) / 100;
}
const change = deltaY * stepSize;
let newValue = startValue + change;
newValue = Math.max( min, Math.min( newValue, max ) );
const precision = ( String( step ).split( '.' )[ 1 ] || [] ).length;
this.input.value = newValue.toFixed( precision );
this.input.dispatchEvent( new Event( 'input' ) );
this.dispatchChange();
}
} );
document.addEventListener( 'mouseup', () => {
if ( isDragging ) {
isDragging = false;
document.body.style.cursor = 'default';
}
} );
}
setValue( val ) {
this.input.value = val;
return super.setValue( val );
}
getValue() {
return parseFloat( this.input.value );
}
}
class ValueCheckbox extends Value {
constructor( { value = false } ) {
super();
const label = document.createElement( 'label' );
label.className = 'custom-checkbox';
const checkbox = document.createElement( 'input' );
checkbox.type = 'checkbox';
checkbox.checked = value;
this.checkbox = checkbox;
const checkmark = document.createElement( 'span' );
checkmark.className = 'checkmark';
label.appendChild( checkbox );
label.appendChild( checkmark );
this.domElement.appendChild( label );
checkbox.addEventListener( 'change', () => {
this.dispatchChange();
} );
}
setValue( val ) {
this.checkbox.value = val;
return super.setValue( val );
}
getValue() {
return this.checkbox.checked;
}
}
class ValueSlider extends Value {
constructor( { value = 0, min = 0, max = 1, step = 0.01 } ) {
super();
this.slider = document.createElement( 'input' );
this.slider.type = 'range';
this.slider.min = min;
this.slider.max = max;
this.slider.step = step;
const numberValue = new ValueNumber( { value, min, max, step } );
this.numberInput = numberValue.input;
this.numberInput.style.flexBasis = '80px';
this.numberInput.style.flexShrink = '0';
this.slider.value = value;
this.domElement.append( this.slider, this.numberInput );
this.slider.addEventListener( 'input', () => {
this.numberInput.value = this.slider.value;
this.dispatchChange();
} );
numberValue.addEventListener( 'change', () => {
this.slider.value = parseFloat( this.numberInput.value );
this.dispatchChange();
} );
}
setValue( val ) {
this.slider.value = val;
this.numberInput.value = val;
return super.setValue( val );
}
getValue() {
return parseFloat( this.slider.value );
}
step( value ) {
this.slider.step = value;
this.numberInput.step = value;
return this;
}
}
class ValueSelect extends Value {
constructor( { options = [], value = '' } ) {
super();
const select = document.createElement( 'select' );
const createOption = ( name, optionValue ) => {
const optionEl = document.createElement( 'option' );
optionEl.value = name;
optionEl.textContent = name;
if ( optionValue == value ) optionEl.selected = true;
select.appendChild( optionEl );
return optionEl;
};
if ( Array.isArray( options ) ) {
options.forEach( opt => createOption( opt, opt ) );
} else {
Object.entries( options ).forEach( ( [ key, value ] ) => createOption( key, value ) );
}
this.domElement.appendChild( select );
//
select.addEventListener( 'change', () => {
this.dispatchChange();
} );
this.options = options;
this.select = select;
}
getValue() {
const options = this.options;
if ( Array.isArray( options ) ) {
return options[ this.select.selectedIndex ];
} else {
return options[ this.select.value ];
}
}
}
class ValueColor extends Value {
constructor( { value = '#ffffff' } ) {
super();
const colorInput = document.createElement( 'input' );
colorInput.type = 'color';
colorInput.value = this._getColorHex( value );
this.colorInput = colorInput;
this._value = value;
colorInput.addEventListener( 'input', () => {
const colorValue = colorInput.value;
if ( this._value.isColor ) {
this._value.setHex( parseInt( colorValue.slice( 1 ), 16 ) );
} else {
this._value = colorValue;
}
this.dispatchChange();
} );
this.domElement.appendChild( colorInput );
}
_getColorHex( color ) {
if ( color.isColor ) {
color = color.getHex();
}
if ( typeof color === 'number' ) {
color = `#${ color.toString( 16 ) }`;
} else if ( color[ 0 ] !== '#' ) {
color = '#' + color;
}
return color;
}
getValue() {
let value = this._value;
if ( typeof value === 'string' ) {
value = parseInt( value.slice( 1 ), 16 );
}
return value;
}
}
class ValueButton extends Value {
constructor( { text = 'Button', value = () => {} } ) {
super();
const button = document.createElement( 'button' );
button.textContent = text;
button.onclick = value;
this.domElement.appendChild( button );
}
}
export { Value, ValueNumber, ValueCheckbox, ValueSlider, ValueSelect, ValueColor, ValueButton };

56
node_modules/three/examples/jsm/inspector/ui/utils.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
export function createValueSpan( id = null ) {
const span = document.createElement( 'span' );
span.className = 'value';
if ( id !== null ) span.id = id;
return span;
}
export function setText( element, text ) {
const el = element instanceof HTMLElement ? element : document.getElementById( element );
if ( el && el.textContent !== text ) {
el.textContent = text;
}
}
export function getText( element ) {
const el = element instanceof HTMLElement ? element : document.getElementById( element );
return el ? el.textContent : null;
}
export function splitPath( fullPath ) {
const lastSlash = fullPath.lastIndexOf( '/' );
if ( lastSlash === - 1 ) {
return {
path: '',
name: fullPath.trim()
};
}
const path = fullPath.substring( 0, lastSlash ).trim();
const name = fullPath.substring( lastSlash + 1 ).trim();
return { path, name };
}
export function splitCamelCase( str ) {
return str.replace( /([a-z0-9])([A-Z])/g, '$1 $2' ).trim();
}