You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

437 lines
8.4 KiB

import { UIElement, UIPanel, UIText } from './libs/ui.js';
import { SetScriptValueCommand } from './commands/SetScriptValueCommand.js';
import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
function Script( editor ) {
const signals = editor.signals;
const container = new UIPanel();
container.setId( 'script' );
container.setPosition( 'absolute' );
container.setBackgroundColor( '#272822' );
container.setDisplay( 'none' );
const header = new UIPanel();
header.setPadding( '10px' );
container.add( header );
const title = new UIText().setColor( '#fff' );
header.add( title );
const buttonSVG = ( function () {
const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
svg.setAttribute( 'width', 32 );
svg.setAttribute( 'height', 32 );
const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
path.setAttribute( 'd', 'M 12,12 L 22,22 M 22,12 12,22' );
path.setAttribute( 'stroke', '#fff' );
svg.appendChild( path );
return svg;
} )();
const close = new UIElement( buttonSVG );
close.setPosition( 'absolute' );
close.setTop( '3px' );
close.setRight( '1px' );
close.setCursor( 'pointer' );
close.onClick( function () {
container.setDisplay( 'none' );
} );
header.add( close );
let renderer;
signals.rendererCreated.add( function ( newRenderer ) {
renderer = newRenderer;
} );
let delay;
let currentMode;
let currentScript;
let currentObject;
const codemirror = CodeMirror( container.dom, {
value: '',
lineNumbers: true,
matchBrackets: true,
indentWithTabs: true,
tabSize: 4,
indentUnit: 4,
hintOptions: {
completeSingle: false
}
} );
codemirror.setOption( 'theme', 'monokai' );
codemirror.on( 'change', function () {
if ( codemirror.state.focused === false ) return;
clearTimeout( delay );
delay = setTimeout( function () {
const value = codemirror.getValue();
if ( ! validate( value ) ) return;
if ( typeof ( currentScript ) === 'object' ) {
if ( value !== currentScript.source ) {
editor.execute( new SetScriptValueCommand( editor, currentObject, currentScript, 'source', value ) );
}
return;
}
if ( currentScript !== 'programInfo' ) return;
const json = JSON.parse( value );
if ( JSON.stringify( currentObject.material.defines ) !== JSON.stringify( json.defines ) ) {
const cmd = new SetMaterialValueCommand( editor, currentObject, 'defines', json.defines );
cmd.updatable = false;
editor.execute( cmd );
}
if ( JSON.stringify( currentObject.material.uniforms ) !== JSON.stringify( json.uniforms ) ) {
const cmd = new SetMaterialValueCommand( editor, currentObject, 'uniforms', json.uniforms );
cmd.updatable = false;
editor.execute( cmd );
}
if ( JSON.stringify( currentObject.material.attributes ) !== JSON.stringify( json.attributes ) ) {
const cmd = new SetMaterialValueCommand( editor, currentObject, 'attributes', json.attributes );
cmd.updatable = false;
editor.execute( cmd );
}
}, 300 );
} );
// prevent backspace from deleting objects
const wrapper = codemirror.getWrapperElement();
wrapper.addEventListener( 'keydown', function ( event ) {
event.stopPropagation();
} );
// validate
const errorLines = [];
const widgets = [];
const validate = function ( string ) {
let valid;
let errors = [];
return codemirror.operation( function () {
while ( errorLines.length > 0 ) {
codemirror.removeLineClass( errorLines.shift(), 'background', 'errorLine' );
}
while ( widgets.length > 0 ) {
codemirror.removeLineWidget( widgets.shift() );
}
//
switch ( currentMode ) {
case 'javascript':
try {
const syntax = esprima.parse( string, { tolerant: true } );
errors = syntax.errors;
} catch ( error ) {
errors.push( {
lineNumber: error.lineNumber - 1,
message: error.message
} );
}
for ( let i = 0; i < errors.length; i ++ ) {
const error = errors[ i ];
error.message = error.message.replace( /Line [0-9]+: /, '' );
}
break;
case 'json':
errors = [];
jsonlint.parseError = function ( message, info ) {
message = message.split( '\n' )[ 3 ];
errors.push( {
lineNumber: info.loc.first_line - 1,
message: message
} );
};
try {
jsonlint.parse( string );
} catch ( error ) {
// ignore failed error recovery
}
break;
case 'glsl':
currentObject.material[ currentScript ] = string;
currentObject.material.needsUpdate = true;
signals.materialChanged.dispatch( currentObject.material );
const programs = renderer.info.programs;
valid = true;
const parseMessage = /^(?:ERROR|WARNING): \d+:(\d+): (.*)/g;
for ( let i = 0, n = programs.length; i !== n; ++ i ) {
const diagnostics = programs[ i ].diagnostics;
if ( diagnostics === undefined ||
diagnostics.material !== currentObject.material ) continue;
if ( ! diagnostics.runnable ) valid = false;
const shaderInfo = diagnostics[ currentScript ];
const lineOffset = shaderInfo.prefix.split( /\r\n|\r|\n/ ).length;
while ( true ) {
const parseResult = parseMessage.exec( shaderInfo.log );
if ( parseResult === null ) break;
errors.push( {
lineNumber: parseResult[ 1 ] - lineOffset,
message: parseResult[ 2 ]
} );
} // messages
break;
} // programs
} // mode switch
for ( let i = 0; i < errors.length; i ++ ) {
const error = errors[ i ];
const message = document.createElement( 'div' );
message.className = 'esprima-error';
message.textContent = error.message;
const lineNumber = Math.max( error.lineNumber, 0 );
errorLines.push( lineNumber );
codemirror.addLineClass( lineNumber, 'background', 'errorLine' );
const widget = codemirror.addLineWidget( lineNumber, message );
widgets.push( widget );
}
return valid !== undefined ? valid : errors.length === 0;
} );
};
// tern js autocomplete
const server = new CodeMirror.TernServer( {
caseInsensitive: true,
plugins: { threejs: null }
} );
codemirror.setOption( 'extraKeys', {
'Ctrl-Space': function ( cm ) {
server.complete( cm );
},
'Ctrl-I': function ( cm ) {
server.showType( cm );
},
'Ctrl-O': function ( cm ) {
server.showDocs( cm );
},
'Alt-.': function ( cm ) {
server.jumpToDef( cm );
},
'Alt-,': function ( cm ) {
server.jumpBack( cm );
},
'Ctrl-Q': function ( cm ) {
server.rename( cm );
},
'Ctrl-.': function ( cm ) {
server.selectName( cm );
}
} );
codemirror.on( 'cursorActivity', function ( cm ) {
if ( currentMode !== 'javascript' ) return;
server.updateArgHints( cm );
} );
codemirror.on( 'keypress', function ( cm, kb ) {
if ( currentMode !== 'javascript' ) return;
const typed = String.fromCharCode( kb.which || kb.keyCode );
if ( /[\w\.]/.exec( typed ) ) {
server.complete( cm );
}
} );
//
signals.editorCleared.add( function () {
container.setDisplay( 'none' );
} );
signals.editScript.add( function ( object, script ) {
let mode, name, source;
if ( typeof ( script ) === 'object' ) {
mode = 'javascript';
name = script.name;
source = script.source;
title.setValue( object.name + ' / ' + name );
} else {
switch ( script ) {
case 'vertexShader':
mode = 'glsl';
name = 'Vertex Shader';
source = object.material.vertexShader || '';
break;
case 'fragmentShader':
mode = 'glsl';
name = 'Fragment Shader';
source = object.material.fragmentShader || '';
break;
case 'programInfo':
mode = 'json';
name = 'Program Properties';
const json = {
defines: object.material.defines,
uniforms: object.material.uniforms,
attributes: object.material.attributes
};
source = JSON.stringify( json, null, '\t' );
}
title.setValue( object.material.name + ' / ' + name );
}
currentMode = mode;
currentScript = script;
currentObject = object;
container.setDisplay( '' );
codemirror.setValue( source );
codemirror.clearHistory();
if ( mode === 'json' ) mode = { name: 'javascript', json: true };
codemirror.setOption( 'mode', mode );
} );
signals.scriptRemoved.add( function ( script ) {
if ( currentScript === script ) {
container.setDisplay( 'none' );
}
} );
return container;
}
export { Script };