bridge.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /**
  2. * This script injected by the installed three.js developer
  3. * tools extension.
  4. */
  5. // Only initialize if not already initialized
  6. if (!window.__THREE_DEVTOOLS__) {
  7. // Create our custom EventTarget with logging
  8. class DevToolsEventTarget extends EventTarget {
  9. constructor() {
  10. super();
  11. this._ready = false;
  12. this._backlog = [];
  13. this.objects = new Map();
  14. }
  15. addEventListener(type, listener, options) {
  16. super.addEventListener(type, listener, options);
  17. // If this is the first listener for a type, and we have backlogged events,
  18. // check if we should process them
  19. if (type !== 'devtools-ready' && this._backlog.length > 0) {
  20. this.dispatchEvent(new CustomEvent('devtools-ready'));
  21. }
  22. }
  23. dispatchEvent(event) {
  24. if (this._ready || event.type === 'devtools-ready') {
  25. if (event.type === 'devtools-ready') {
  26. this._ready = true;
  27. const backlog = this._backlog;
  28. this._backlog = [];
  29. backlog.forEach(e => super.dispatchEvent(e));
  30. }
  31. return super.dispatchEvent(event);
  32. } else {
  33. this._backlog.push(event);
  34. return false; // Return false to indicate synchronous handling
  35. }
  36. }
  37. reset() {
  38. // console.log('DevTools: Resetting state');
  39. // Clear objects map
  40. this.objects.clear();
  41. // Clear backlog
  42. this._backlog = [];
  43. // Reset ready state
  44. this._ready = false;
  45. // Clear observed arrays
  46. observedScenes.length = 0;
  47. observedRenderers.length = 0;
  48. }
  49. }
  50. // Create and expose the __THREE_DEVTOOLS__ object
  51. const devTools = new DevToolsEventTarget();
  52. Object.defineProperty(window, '__THREE_DEVTOOLS__', {
  53. value: devTools,
  54. configurable: false,
  55. enumerable: true,
  56. writable: false
  57. });
  58. // Declare arrays for tracking observed objects
  59. const observedScenes = [];
  60. const observedRenderers = [];
  61. // Function to get renderer data
  62. function getRendererData(renderer) {
  63. try {
  64. const webglInfo = getWebGLInfo(renderer);
  65. const data = {
  66. uuid: renderer.uuid || generateUUID(),
  67. type: 'WebGLRenderer',
  68. name: '',
  69. visible: true,
  70. isScene: false,
  71. isObject3D: false,
  72. isCamera: false,
  73. isLight: false,
  74. isMesh: false,
  75. isRenderer: true,
  76. parent: null,
  77. children: [],
  78. properties: {
  79. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  80. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  81. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  82. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  83. alpha: renderer.alpha || false,
  84. antialias: renderer.antialias || false,
  85. autoClear: renderer.autoClear,
  86. autoClearColor: renderer.autoClearColor,
  87. autoClearDepth: renderer.autoClearDepth,
  88. autoClearStencil: renderer.autoClearStencil,
  89. localClippingEnabled: renderer.localClippingEnabled,
  90. physicallyCorrectLights: renderer.physicallyCorrectLights,
  91. outputColorSpace: renderer.outputColorSpace,
  92. toneMapping: renderer.toneMapping,
  93. toneMappingExposure: renderer.toneMappingExposure,
  94. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  95. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  96. info: {
  97. render: {
  98. frame: renderer.info.render.frame,
  99. calls: renderer.info.render.calls,
  100. triangles: renderer.info.render.triangles,
  101. points: renderer.info.render.points,
  102. lines: renderer.info.render.lines,
  103. geometries: renderer.info.render.geometries,
  104. sprites: renderer.info.render.sprites
  105. },
  106. memory: {
  107. geometries: renderer.info.memory.geometries,
  108. textures: renderer.info.memory.textures,
  109. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  110. renderLists: renderer.info.memory.renderLists,
  111. renderTargets: renderer.info.memory.renderTargets
  112. },
  113. webgl: webglInfo || {
  114. version: 'unknown',
  115. gpu: 'unknown',
  116. vendor: 'unknown',
  117. maxTextures: 'unknown',
  118. maxAttributes: 'unknown',
  119. maxTextureSize: 'unknown',
  120. maxCubemapSize: 'unknown'
  121. }
  122. }
  123. }
  124. };
  125. return data;
  126. } catch (error) {
  127. console.warn('DevTools: Error getting renderer data:', error);
  128. return null;
  129. }
  130. }
  131. // Function to get object hierarchy
  132. function getObjectData(obj) {
  133. try {
  134. // Special case for WebGLRenderer
  135. if (obj.isWebGLRenderer === true) {
  136. return getRendererData(obj);
  137. }
  138. // Get descriptive name for the object
  139. let name = obj.name || obj.type || obj.constructor.name;
  140. if (obj.isMesh) {
  141. const geoType = obj.geometry ? obj.geometry.type : 'Unknown';
  142. const matType = obj.material ?
  143. (Array.isArray(obj.material) ?
  144. obj.material.map(m => m.type).join(', ') :
  145. obj.material.type) :
  146. 'Unknown';
  147. name = `${name} <span class="object-details">${geoType} ${matType}</span>`;
  148. }
  149. const data = {
  150. uuid: obj.uuid,
  151. type: obj.type || obj.constructor.name,
  152. name: name,
  153. visible: obj.visible !== undefined ? obj.visible : true,
  154. isScene: obj.isScene === true,
  155. isObject3D: obj.isObject3D === true,
  156. isCamera: obj.isCamera === true,
  157. isLight: obj.isLight === true,
  158. isMesh: obj.isMesh === true,
  159. isRenderer: obj.isWebGLRenderer === true,
  160. parent: obj.parent ? obj.parent.uuid : null,
  161. children: obj.children ? obj.children.map(child => child.uuid) : []
  162. };
  163. return data;
  164. } catch (error) {
  165. console.warn('DevTools: Error getting object data:', error);
  166. return null;
  167. }
  168. }
  169. // Generate a UUID for objects that don't have one
  170. function generateUUID() {
  171. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  172. const r = Math.random() * 16 | 0;
  173. const v = c === 'x' ? r : (r & 0x3 | 0x8);
  174. return v.toString(16);
  175. });
  176. }
  177. // Listen for Three.js registration
  178. devTools.addEventListener('register', (event) => {
  179. // console.log('DevTools: Three.js registered with revision:', event.detail.revision);
  180. dispatchEvent('register', event.detail);
  181. });
  182. // Listen for object observations
  183. devTools.addEventListener('observe', (event) => {
  184. const obj = event.detail;
  185. if (!obj) {
  186. console.warn('DevTools: Received observe event with null/undefined detail');
  187. return;
  188. }
  189. // Generate UUID if needed
  190. if (!obj.uuid) {
  191. obj.uuid = generateUUID();
  192. }
  193. // Skip if already registered (essential to prevent loops with batching)
  194. if (devTools.objects.has(obj.uuid)) {
  195. return;
  196. }
  197. // Handle Renderers individually
  198. if (obj.isWebGLRenderer) {
  199. const data = getObjectData(obj);
  200. if (data) {
  201. data.properties = getRendererProperties(obj);
  202. observedRenderers.push(obj);
  203. devTools.objects.set(obj.uuid, data); // Store locally
  204. dispatchEvent('renderer', data); // Send to panel as 'renderer'
  205. }
  206. }
  207. // Handle Scenes via batch
  208. else if (obj.isScene) {
  209. // Don't add scene to devTools.objects here yet, batch will handle it
  210. observedScenes.push(obj); // Track the scene object
  211. const batchObjects = [];
  212. const processedUUIDs = new Set();
  213. function traverseForBatch(currentObj) {
  214. if (!currentObj || !currentObj.uuid || processedUUIDs.has(currentObj.uuid)) return;
  215. processedUUIDs.add(currentObj.uuid);
  216. const objectData = getObjectData(currentObj);
  217. if (objectData) {
  218. batchObjects.push(objectData);
  219. devTools.objects.set(currentObj.uuid, objectData); // Update local cache during batch creation
  220. }
  221. // Process children
  222. if (currentObj.children && Array.isArray(currentObj.children)) {
  223. currentObj.children.forEach(child => traverseForBatch(child));
  224. }
  225. }
  226. traverseForBatch(obj); // Start traversal from the scene
  227. // Dispatch the batch as 'scene'
  228. dispatchEvent('scene', { sceneUuid: obj.uuid, objects: batchObjects });
  229. }
  230. // Ignore other object types arriving directly via 'observe'?
  231. // They should be discovered via scene traversal.
  232. });
  233. // Function to get renderer properties
  234. function getRendererProperties(renderer) {
  235. const webglInfo = getWebGLInfo(renderer);
  236. const context = renderer.getContext ? renderer.getContext() : null;
  237. const contextAttributes = context ? context.getContextAttributes() : null;
  238. return {
  239. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  240. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  241. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  242. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  243. alpha: contextAttributes ? contextAttributes.alpha : false,
  244. antialias: contextAttributes ? contextAttributes.antialias : false,
  245. autoClear: renderer.autoClear,
  246. autoClearColor: renderer.autoClearColor,
  247. autoClearDepth: renderer.autoClearDepth,
  248. autoClearStencil: renderer.autoClearStencil,
  249. localClippingEnabled: renderer.localClippingEnabled,
  250. physicallyCorrectLights: renderer.physicallyCorrectLights,
  251. outputColorSpace: renderer.outputColorSpace,
  252. toneMapping: renderer.toneMapping,
  253. toneMappingExposure: renderer.toneMappingExposure,
  254. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  255. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  256. info: {
  257. render: {
  258. frame: renderer.info.render.frame,
  259. calls: renderer.info.render.calls,
  260. triangles: renderer.info.render.triangles,
  261. points: renderer.info.render.points,
  262. lines: renderer.info.render.lines,
  263. geometries: renderer.info.render.geometries,
  264. sprites: renderer.info.render.sprites
  265. },
  266. memory: {
  267. geometries: renderer.info.memory.geometries,
  268. textures: renderer.info.memory.textures,
  269. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  270. renderLists: renderer.info.memory.renderLists,
  271. renderTargets: renderer.info.memory.renderTargets
  272. },
  273. webgl: webglInfo || {
  274. version: 'unknown',
  275. gpu: 'unknown',
  276. vendor: 'unknown',
  277. maxTextures: 'unknown',
  278. maxAttributes: 'unknown',
  279. maxTextureSize: 'unknown',
  280. maxCubemapSize: 'unknown'
  281. }
  282. }
  283. };
  284. }
  285. // Start periodic renderer checks
  286. // console.log('DevTools: Starting periodic renderer checks');
  287. // Function to check if bridge is available
  288. function checkBridgeAvailability() {
  289. const hasDevTools = window.hasOwnProperty('__THREE_DEVTOOLS__');
  290. const devToolsValue = window.__THREE_DEVTOOLS__;
  291. // If we have devtools and we're interactive or complete, trigger ready
  292. if (hasDevTools && devToolsValue && (document.readyState === 'interactive' || document.readyState === 'complete')) {
  293. devTools.dispatchEvent(new CustomEvent('devtools-ready'));
  294. }
  295. }
  296. // Watch for readyState changes
  297. document.addEventListener('readystatechange', () => {
  298. if (document.readyState === 'loading') {
  299. devTools.reset();
  300. }
  301. checkBridgeAvailability();
  302. });
  303. // Watch for page unload to reset state
  304. window.addEventListener('beforeunload', () => {
  305. devTools.reset();
  306. });
  307. // Listen for messages from the content script
  308. window.addEventListener('message', function(event) {
  309. // Only accept messages from the same frame
  310. if (event.source !== window) return;
  311. const message = event.data;
  312. if (!message || message.id !== 'three-devtools') return;
  313. // Handle request for initial state from panel
  314. if ( message.name === 'request-initial-state' ) {
  315. for (const observedRenderer of observedRenderers) {
  316. const data = getObjectData(observedRenderer);
  317. if (data) {
  318. data.properties = getRendererProperties(observedRenderer);
  319. dispatchEvent('renderer', data);
  320. }
  321. }
  322. for (const observedScene of observedScenes) {
  323. reloadSceneObjects(observedScene);
  324. }
  325. }
  326. });
  327. // Helper function to find a Three.js object by UUID
  328. function findObjectByUUID(uuid) {
  329. console.log('DevTools: Finding object by UUID:', uuid);
  330. // Check for scenes we've observed
  331. const sceneData = Array.from(devTools.objects.values())
  332. .find(obj => obj.uuid === uuid && obj.isScene);
  333. if (sceneData) {
  334. // For scenes accessed through observe events, they are already available
  335. // through the scene object reference passed to the observe handler
  336. for (const observedScene of observedScenes) {
  337. if (observedScene && observedScene.uuid === uuid) {
  338. console.log('DevTools: Found scene in observed scenes');
  339. return observedScene;
  340. }
  341. }
  342. }
  343. console.warn('DevTools: Could not find object with UUID:', uuid);
  344. return null;
  345. }
  346. function dispatchEvent(type, detail) {
  347. try {
  348. window.postMessage({
  349. id: 'three-devtools',
  350. type: type,
  351. detail: detail
  352. }, '*');
  353. } catch (error) {
  354. // If we get an "Extension context invalidated" error, stop all monitoring
  355. if (error.message.includes('Extension context invalidated')) {
  356. console.log('DevTools: Extension context invalidated, stopping monitoring');
  357. devTools.reset();
  358. return;
  359. }
  360. console.warn('DevTools: Error dispatching event:', error);
  361. }
  362. }
  363. function getWebGLInfo(renderer) {
  364. if (!renderer || !renderer.domElement) return null;
  365. const gl = renderer.domElement.getContext('webgl2') || renderer.domElement.getContext('webgl');
  366. if (!gl) return null;
  367. return {
  368. version: gl.getParameter(gl.VERSION),
  369. gpu: gl.getParameter(gl.RENDERER),
  370. vendor: gl.getParameter(gl.VENDOR),
  371. maxTextures: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
  372. maxAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
  373. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
  374. maxCubemapSize: gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)
  375. };
  376. }
  377. // Function to manually reload scene objects
  378. function reloadSceneObjects(scene) {
  379. // console.log('DevTools: Manually reloading scene objects for scene:', scene.uuid);
  380. const batchObjects = [];
  381. const processedUUIDs = new Set(); // Track processed objects within this refresh
  382. const currentUUIDsInScene = new Set(); // Track objects found in this traversal
  383. // Temporarily store old UUIDs known to be under this scene to detect removals
  384. const oldUUIDsInScene = new Set();
  385. devTools.objects.forEach((objData, uuid) => {
  386. if (!objData.isRenderer && (objData.uuid === scene.uuid || objData.parent === scene.uuid)) { // Simple check, might need recursive parent check for deeper trees
  387. oldUUIDsInScene.add(uuid);
  388. }
  389. });
  390. // Recursively observe all objects, collect data, update local cache
  391. function observeAndBatchObject(object) {
  392. if (!object || !object.uuid || processedUUIDs.has(object.uuid)) return;
  393. processedUUIDs.add(object.uuid);
  394. currentUUIDsInScene.add(object.uuid); // Mark as present
  395. // console.log('DevTools: Processing object during reload:', object.type || object.constructor.name, object.uuid);
  396. // Get object data
  397. const objectData = getObjectData(object);
  398. if (objectData) {
  399. batchObjects.push(objectData); // Add to batch
  400. // Update or add to local cache immediately
  401. devTools.objects.set(object.uuid, objectData);
  402. }
  403. // Process children recursively
  404. if (object.children && Array.isArray(object.children)) {
  405. // console.log('DevTools: Processing', object.children.length, 'children of', object.type || object.constructor.name);
  406. object.children.forEach(child => observeAndBatchObject(child));
  407. }
  408. }
  409. // Start traversal from the scene itself
  410. observeAndBatchObject(scene);
  411. // Dispatch the batch update for the panel as 'scene'
  412. dispatchEvent('scene', { sceneUuid: scene.uuid, objects: batchObjects });
  413. // TODO: Optionally, detect and dispatch 'remove' events here?
  414. // For now, panel handles removal implicitly based on batch content.
  415. // console.log('DevTools: Scene reload batch dispatched. Processed', processedUUIDs.size, 'objects');
  416. }
  417. } else {
  418. // console.log('DevTools: Bridge already initialized');
  419. }
粤ICP备19079148号