Script.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. /**
  2. * @author mrdoob / http://mrdoob.com/
  3. */
  4. import { UIElement, UIPanel, UIText } from './libs/ui.js';
  5. import { SetScriptValueCommand } from './commands/SetScriptValueCommand.js';
  6. import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
  7. var Script = function ( editor ) {
  8. var signals = editor.signals;
  9. var container = new UIPanel();
  10. container.setId( 'script' );
  11. container.setPosition( 'absolute' );
  12. container.setBackgroundColor( '#272822' );
  13. container.setDisplay( 'none' );
  14. var header = new UIPanel();
  15. header.setPadding( '10px' );
  16. container.add( header );
  17. var title = new UIText().setColor( '#fff' );
  18. header.add( title );
  19. var buttonSVG = ( function () {
  20. var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
  21. svg.setAttribute( 'width', 32 );
  22. svg.setAttribute( 'height', 32 );
  23. var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
  24. path.setAttribute( 'd', 'M 12,12 L 22,22 M 22,12 12,22' );
  25. path.setAttribute( 'stroke', '#fff' );
  26. svg.appendChild( path );
  27. return svg;
  28. } )();
  29. var close = new UIElement( buttonSVG );
  30. close.setPosition( 'absolute' );
  31. close.setTop( '3px' );
  32. close.setRight( '1px' );
  33. close.setCursor( 'pointer' );
  34. close.onClick( function () {
  35. container.setDisplay( 'none' );
  36. } );
  37. header.add( close );
  38. var renderer;
  39. signals.rendererChanged.add( function ( newRenderer ) {
  40. renderer = newRenderer;
  41. } );
  42. var delay;
  43. var currentMode;
  44. var currentScript;
  45. var currentObject;
  46. var codemirror = CodeMirror( container.dom, {
  47. value: '',
  48. lineNumbers: true,
  49. matchBrackets: true,
  50. indentWithTabs: true,
  51. tabSize: 4,
  52. indentUnit: 4,
  53. hintOptions: {
  54. completeSingle: false
  55. }
  56. } );
  57. codemirror.setOption( 'theme', 'monokai' );
  58. codemirror.on( 'change', function () {
  59. if ( codemirror.state.focused === false ) return;
  60. clearTimeout( delay );
  61. delay = setTimeout( function () {
  62. var value = codemirror.getValue();
  63. if ( ! validate( value ) ) return;
  64. if ( typeof ( currentScript ) === 'object' ) {
  65. if ( value !== currentScript.source ) {
  66. editor.execute( new SetScriptValueCommand( editor, currentObject, currentScript, 'source', value ) );
  67. }
  68. return;
  69. }
  70. if ( currentScript !== 'programInfo' ) return;
  71. var json = JSON.parse( value );
  72. if ( JSON.stringify( currentObject.material.defines ) !== JSON.stringify( json.defines ) ) {
  73. var cmd = new SetMaterialValueCommand( editor, currentObject, 'defines', json.defines );
  74. cmd.updatable = false;
  75. editor.execute( cmd );
  76. }
  77. if ( JSON.stringify( currentObject.material.uniforms ) !== JSON.stringify( json.uniforms ) ) {
  78. var cmd = new SetMaterialValueCommand( editor, currentObject, 'uniforms', json.uniforms );
  79. cmd.updatable = false;
  80. editor.execute( cmd );
  81. }
  82. if ( JSON.stringify( currentObject.material.attributes ) !== JSON.stringify( json.attributes ) ) {
  83. var cmd = new SetMaterialValueCommand( editor, currentObject, 'attributes', json.attributes );
  84. cmd.updatable = false;
  85. editor.execute( cmd );
  86. }
  87. }, 300 );
  88. } );
  89. // prevent backspace from deleting objects
  90. var wrapper = codemirror.getWrapperElement();
  91. wrapper.addEventListener( 'keydown', function ( event ) {
  92. event.stopPropagation();
  93. } );
  94. // validate
  95. var errorLines = [];
  96. var widgets = [];
  97. var validate = function ( string ) {
  98. var valid;
  99. var errors = [];
  100. return codemirror.operation( function () {
  101. while ( errorLines.length > 0 ) {
  102. codemirror.removeLineClass( errorLines.shift(), 'background', 'errorLine' );
  103. }
  104. while ( widgets.length > 0 ) {
  105. codemirror.removeLineWidget( widgets.shift() );
  106. }
  107. //
  108. switch ( currentMode ) {
  109. case 'javascript':
  110. try {
  111. var syntax = esprima.parse( string, { tolerant: true } );
  112. errors = syntax.errors;
  113. } catch ( error ) {
  114. errors.push( {
  115. lineNumber: error.lineNumber - 1,
  116. message: error.message
  117. } );
  118. }
  119. for ( var i = 0; i < errors.length; i ++ ) {
  120. var error = errors[ i ];
  121. error.message = error.message.replace( /Line [0-9]+: /, '' );
  122. }
  123. break;
  124. case 'json':
  125. errors = [];
  126. jsonlint.parseError = function ( message, info ) {
  127. message = message.split( '\n' )[ 3 ];
  128. errors.push( {
  129. lineNumber: info.loc.first_line - 1,
  130. message: message
  131. } );
  132. };
  133. try {
  134. jsonlint.parse( string );
  135. } catch ( error ) {
  136. // ignore failed error recovery
  137. }
  138. break;
  139. case 'glsl':
  140. try {
  141. var shaderType = currentScript === 'vertexShader' ?
  142. glslprep.Shader.VERTEX : glslprep.Shader.FRAGMENT;
  143. glslprep.parseGlsl( string, shaderType );
  144. } catch ( error ) {
  145. if ( error instanceof glslprep.SyntaxError ) {
  146. errors.push( {
  147. lineNumber: error.line,
  148. message: "Syntax Error: " + error.message
  149. } );
  150. } else {
  151. console.error( error.stack || error );
  152. }
  153. }
  154. if ( errors.length !== 0 ) break;
  155. if ( renderer instanceof THREE.WebGLRenderer === false ) break;
  156. currentObject.material[ currentScript ] = string;
  157. currentObject.material.needsUpdate = true;
  158. signals.materialChanged.dispatch( currentObject.material );
  159. var programs = renderer.info.programs;
  160. valid = true;
  161. var parseMessage = /^(?:ERROR|WARNING): \d+:(\d+): (.*)/g;
  162. for ( var i = 0, n = programs.length; i !== n; ++ i ) {
  163. var diagnostics = programs[ i ].diagnostics;
  164. if ( diagnostics === undefined ||
  165. diagnostics.material !== currentObject.material ) continue;
  166. if ( ! diagnostics.runnable ) valid = false;
  167. var shaderInfo = diagnostics[ currentScript ];
  168. var lineOffset = shaderInfo.prefix.split( /\r\n|\r|\n/ ).length;
  169. while ( true ) {
  170. var parseResult = parseMessage.exec( shaderInfo.log );
  171. if ( parseResult === null ) break;
  172. errors.push( {
  173. lineNumber: parseResult[ 1 ] - lineOffset,
  174. message: parseResult[ 2 ]
  175. } );
  176. } // messages
  177. break;
  178. } // programs
  179. } // mode switch
  180. for ( var i = 0; i < errors.length; i ++ ) {
  181. var error = errors[ i ];
  182. var message = document.createElement( 'div' );
  183. message.className = 'esprima-error';
  184. message.textContent = error.message;
  185. var lineNumber = Math.max( error.lineNumber, 0 );
  186. errorLines.push( lineNumber );
  187. codemirror.addLineClass( lineNumber, 'background', 'errorLine' );
  188. var widget = codemirror.addLineWidget( lineNumber, message );
  189. widgets.push( widget );
  190. }
  191. return valid !== undefined ? valid : errors.length === 0;
  192. } );
  193. };
  194. // tern js autocomplete
  195. var server = new CodeMirror.TernServer( {
  196. caseInsensitive: true,
  197. plugins: { threejs: null }
  198. } );
  199. codemirror.setOption( 'extraKeys', {
  200. 'Ctrl-Space': function ( cm ) {
  201. server.complete( cm );
  202. },
  203. 'Ctrl-I': function ( cm ) {
  204. server.showType( cm );
  205. },
  206. 'Ctrl-O': function ( cm ) {
  207. server.showDocs( cm );
  208. },
  209. 'Alt-.': function ( cm ) {
  210. server.jumpToDef( cm );
  211. },
  212. 'Alt-,': function ( cm ) {
  213. server.jumpBack( cm );
  214. },
  215. 'Ctrl-Q': function ( cm ) {
  216. server.rename( cm );
  217. },
  218. 'Ctrl-.': function ( cm ) {
  219. server.selectName( cm );
  220. }
  221. } );
  222. codemirror.on( 'cursorActivity', function ( cm ) {
  223. if ( currentMode !== 'javascript' ) return;
  224. server.updateArgHints( cm );
  225. } );
  226. codemirror.on( 'keypress', function ( cm, kb ) {
  227. if ( currentMode !== 'javascript' ) return;
  228. var typed = String.fromCharCode( kb.which || kb.keyCode );
  229. if ( /[\w\.]/.exec( typed ) ) {
  230. server.complete( cm );
  231. }
  232. } );
  233. //
  234. signals.editorCleared.add( function () {
  235. container.setDisplay( 'none' );
  236. } );
  237. signals.editScript.add( function ( object, script ) {
  238. var mode, name, source;
  239. if ( typeof ( script ) === 'object' ) {
  240. mode = 'javascript';
  241. name = script.name;
  242. source = script.source;
  243. title.setValue( object.name + ' / ' + name );
  244. } else {
  245. switch ( script ) {
  246. case 'vertexShader':
  247. mode = 'glsl';
  248. name = 'Vertex Shader';
  249. source = object.material.vertexShader || "";
  250. break;
  251. case 'fragmentShader':
  252. mode = 'glsl';
  253. name = 'Fragment Shader';
  254. source = object.material.fragmentShader || "";
  255. break;
  256. case 'programInfo':
  257. mode = 'json';
  258. name = 'Program Properties';
  259. var json = {
  260. defines: object.material.defines,
  261. uniforms: object.material.uniforms,
  262. attributes: object.material.attributes
  263. };
  264. source = JSON.stringify( json, null, '\t' );
  265. }
  266. title.setValue( object.material.name + ' / ' + name );
  267. }
  268. currentMode = mode;
  269. currentScript = script;
  270. currentObject = object;
  271. container.setDisplay( '' );
  272. codemirror.setValue( source );
  273. codemirror.clearHistory();
  274. if ( mode === 'json' ) mode = { name: 'javascript', json: true };
  275. codemirror.setOption( 'mode', mode );
  276. } );
  277. signals.scriptRemoved.add( function ( script ) {
  278. if ( currentScript === script ) {
  279. container.setDisplay( 'none' );
  280. }
  281. } );
  282. return container;
  283. };
  284. export { Script };