Script.js 10 KB

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