123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- 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 strings = editor.strings;
- 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, 0 ); // TODO: Add multi-material support
- 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;
- if ( /[\w\.]/.exec( kb.key ) ) {
- server.complete( cm );
- }
- } );
- //
- signals.editorCleared.add( function () {
- container.setDisplay( 'none' );
- } );
- function setTitle( object, script ) {
- if ( typeof script === 'object' ) {
- title.setValue( object.name + ' / ' + script.name );
- } else {
- switch ( script ) {
- case 'vertexShader':
- title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/vertexShader' ) );
- break;
- case 'fragmentShader':
- title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/fragmentShader' ) );
- break;
- case 'programInfo':
- title.setValue( object.material.name + ' / ' + strings.getKey( 'script/title/programInfo' ) );
- break;
- default:
- throw new Error( 'setTitle: Unknown script' );
- }
- }
- }
- signals.editScript.add( function ( object, script ) {
- let mode, source;
- if ( typeof ( script ) === 'object' ) {
- mode = 'javascript';
- source = script.source;
- } else {
- switch ( script ) {
- case 'vertexShader':
- mode = 'glsl';
- source = object.material.vertexShader || '';
- break;
- case 'fragmentShader':
- mode = 'glsl';
- source = object.material.fragmentShader || '';
- break;
- case 'programInfo':
- mode = 'json';
- const json = {
- defines: object.material.defines,
- uniforms: object.material.uniforms,
- attributes: object.material.attributes
- };
- source = JSON.stringify( json, null, '\t' );
- break;
- default:
- throw new Error( 'editScript: Unknown script' );
- }
- }
- setTitle( object, script );
- 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' );
- }
- } );
- signals.objectChanged.add( function ( object ) {
- if ( object !== currentObject ) return;
- if ( [ 'programInfo', 'vertexShader', 'fragmentShader' ].includes( currentScript ) ) return;
- setTitle( currentObject, currentScript );
- } );
- signals.scriptChanged.add( function ( script ) {
- if ( script === currentScript ) {
- setTitle( currentObject, currentScript );
- }
- } );
- signals.materialChanged.add( function ( object, slot ) {
- if ( object !== currentObject ) return;
- // TODO: Adds multi-material support
- setTitle( currentObject, currentScript );
- } );
- return container;
- }
- export { Script };
|