bridge.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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. const sceneObjectCountCache = new Map(); // Cache for object counts per scene
  62. // Function to get renderer data
  63. function getRendererData(renderer) {
  64. try {
  65. const webglInfo = getWebGLInfo(renderer);
  66. const data = {
  67. uuid: renderer.uuid || generateUUID(),
  68. type: 'WebGLRenderer',
  69. name: '',
  70. properties: {
  71. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  72. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  73. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  74. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  75. alpha: renderer.alpha || false,
  76. antialias: renderer.antialias || false,
  77. autoClear: renderer.autoClear,
  78. autoClearColor: renderer.autoClearColor,
  79. autoClearDepth: renderer.autoClearDepth,
  80. autoClearStencil: renderer.autoClearStencil,
  81. localClippingEnabled: renderer.localClippingEnabled,
  82. physicallyCorrectLights: renderer.physicallyCorrectLights,
  83. outputColorSpace: renderer.outputColorSpace,
  84. toneMapping: renderer.toneMapping,
  85. toneMappingExposure: renderer.toneMappingExposure,
  86. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  87. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  88. info: {
  89. render: {
  90. frame: renderer.info.render.frame,
  91. calls: renderer.info.render.calls,
  92. triangles: renderer.info.render.triangles,
  93. points: renderer.info.render.points,
  94. lines: renderer.info.render.lines,
  95. geometries: renderer.info.render.geometries,
  96. sprites: renderer.info.render.sprites
  97. },
  98. memory: {
  99. geometries: renderer.info.memory.geometries,
  100. textures: renderer.info.memory.textures,
  101. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  102. renderLists: renderer.info.memory.renderLists,
  103. renderTargets: renderer.info.memory.renderTargets
  104. },
  105. webgl: webglInfo || {
  106. version: 'unknown',
  107. gpu: 'unknown',
  108. vendor: 'unknown',
  109. maxTextures: 'unknown',
  110. maxAttributes: 'unknown',
  111. maxTextureSize: 'unknown',
  112. maxCubemapSize: 'unknown'
  113. }
  114. }
  115. }
  116. };
  117. return data;
  118. } catch (error) {
  119. console.warn('DevTools: Error getting renderer data:', error);
  120. return null;
  121. }
  122. }
  123. // Function to get object hierarchy
  124. function getObjectData(obj) {
  125. try {
  126. // Special case for WebGLRenderer
  127. if (obj.isWebGLRenderer === true) {
  128. return getRendererData(obj);
  129. }
  130. // Special case for InstancedMesh
  131. let type = obj.isInstancedMesh ? 'InstancedMesh' : obj.type || obj.constructor.name;
  132. // Get descriptive name for the object
  133. let name = obj.name || type || obj.constructor.name;
  134. if (obj.isMesh) {
  135. const geoType = obj.geometry ? obj.geometry.type : 'Unknown';
  136. const matType = obj.material ?
  137. (Array.isArray(obj.material) ?
  138. obj.material.map(m => m.type).join(', ') :
  139. obj.material.type) :
  140. 'Unknown';
  141. if (obj.isInstancedMesh) {
  142. name = `${name} [${obj.count}]`;
  143. }
  144. name = `${name} <span class="object-details">${geoType} ${matType}</span>`;
  145. }
  146. const data = {
  147. uuid: obj.uuid,
  148. name: name,
  149. type: type,
  150. visible: obj.visible !== undefined ? obj.visible : true,
  151. isScene: obj.isScene === true,
  152. isObject3D: obj.isObject3D === true,
  153. isCamera: obj.isCamera === true,
  154. isLight: obj.isLight === true,
  155. isMesh: obj.isMesh === true,
  156. isInstancedMesh: obj.isInstancedMesh === true,
  157. parent: obj.parent ? obj.parent.uuid : null,
  158. children: obj.children ? obj.children.map(child => child.uuid) : []
  159. };
  160. return data;
  161. } catch (error) {
  162. console.warn('DevTools: Error getting object data:', error);
  163. return null;
  164. }
  165. }
  166. // Generate a UUID for objects that don't have one
  167. function generateUUID() {
  168. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  169. const r = Math.random() * 16 | 0;
  170. const v = c === 'x' ? r : (r & 0x3 | 0x8);
  171. return v.toString(16);
  172. });
  173. }
  174. // Listen for Three.js registration
  175. devTools.addEventListener('register', (event) => {
  176. // console.log('DevTools: Three.js registered with revision:', event.detail.revision);
  177. dispatchEvent('register', event.detail);
  178. });
  179. // Listen for object observations
  180. devTools.addEventListener('observe', (event) => {
  181. const obj = event.detail;
  182. if (!obj) {
  183. console.warn('DevTools: Received observe event with null/undefined detail');
  184. return;
  185. }
  186. // Generate UUID if needed
  187. if (!obj.uuid) {
  188. obj.uuid = generateUUID();
  189. }
  190. // Skip if already registered (essential to prevent loops with batching)
  191. if (devTools.objects.has(obj.uuid)) {
  192. return;
  193. }
  194. // Handle Renderers individually
  195. if (obj.isWebGLRenderer) {
  196. const data = getObjectData(obj);
  197. if (data) {
  198. data.properties = getRendererProperties(obj);
  199. observedRenderers.push(obj);
  200. devTools.objects.set(obj.uuid, data); // Store locally
  201. dispatchEvent('renderer', data);
  202. }
  203. }
  204. // Handle Scenes via batch
  205. else if (obj.isScene) {
  206. // Don't add scene to devTools.objects here yet, batch will handle it
  207. observedScenes.push(obj); // Track the scene object
  208. const batchObjects = [];
  209. const processedUUIDs = new Set();
  210. function traverseForBatch(currentObj) {
  211. if (!currentObj || !currentObj.uuid || processedUUIDs.has(currentObj.uuid)) return;
  212. processedUUIDs.add(currentObj.uuid);
  213. const objectData = getObjectData(currentObj);
  214. if (objectData) {
  215. batchObjects.push(objectData);
  216. devTools.objects.set(currentObj.uuid, objectData); // Update local cache during batch creation
  217. }
  218. // Process children
  219. if (currentObj.children && Array.isArray(currentObj.children)) {
  220. currentObj.children.forEach(child => traverseForBatch(child));
  221. }
  222. }
  223. traverseForBatch(obj); // Start traversal from the scene
  224. dispatchEvent('scene', { sceneUuid: obj.uuid, objects: batchObjects });
  225. }
  226. });
  227. // Function to get renderer properties
  228. function getRendererProperties(renderer) {
  229. const webglInfo = getWebGLInfo(renderer);
  230. const context = renderer.getContext ? renderer.getContext() : null;
  231. const contextAttributes = context ? context.getContextAttributes() : null;
  232. return {
  233. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  234. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  235. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  236. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  237. alpha: contextAttributes ? contextAttributes.alpha : false,
  238. antialias: contextAttributes ? contextAttributes.antialias : false,
  239. autoClear: renderer.autoClear,
  240. autoClearColor: renderer.autoClearColor,
  241. autoClearDepth: renderer.autoClearDepth,
  242. autoClearStencil: renderer.autoClearStencil,
  243. localClippingEnabled: renderer.localClippingEnabled,
  244. physicallyCorrectLights: renderer.physicallyCorrectLights,
  245. outputColorSpace: renderer.outputColorSpace,
  246. toneMapping: renderer.toneMapping,
  247. toneMappingExposure: renderer.toneMappingExposure,
  248. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  249. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  250. info: {
  251. render: {
  252. frame: renderer.info.render.frame,
  253. calls: renderer.info.render.calls,
  254. triangles: renderer.info.render.triangles,
  255. points: renderer.info.render.points,
  256. lines: renderer.info.render.lines,
  257. geometries: renderer.info.render.geometries,
  258. sprites: renderer.info.render.sprites
  259. },
  260. memory: {
  261. geometries: renderer.info.memory.geometries,
  262. textures: renderer.info.memory.textures,
  263. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  264. renderLists: renderer.info.memory.renderLists,
  265. renderTargets: renderer.info.memory.renderTargets
  266. },
  267. webgl: webglInfo || {
  268. version: 'unknown',
  269. gpu: 'unknown',
  270. vendor: 'unknown',
  271. maxTextures: 'unknown',
  272. maxAttributes: 'unknown',
  273. maxTextureSize: 'unknown',
  274. maxCubemapSize: 'unknown'
  275. }
  276. }
  277. };
  278. }
  279. // Start periodic renderer checks
  280. // console.log('DevTools: Starting periodic renderer checks');
  281. // Function to check if bridge is available
  282. function checkBridgeAvailability() {
  283. const hasDevTools = window.hasOwnProperty('__THREE_DEVTOOLS__');
  284. const devToolsValue = window.__THREE_DEVTOOLS__;
  285. // If we have devtools and we're interactive or complete, trigger ready
  286. if (hasDevTools && devToolsValue && (document.readyState === 'interactive' || document.readyState === 'complete')) {
  287. devTools.dispatchEvent(new CustomEvent('devtools-ready'));
  288. }
  289. }
  290. // Watch for readyState changes
  291. document.addEventListener('readystatechange', () => {
  292. if (document.readyState === 'loading') {
  293. devTools.reset();
  294. }
  295. checkBridgeAvailability();
  296. });
  297. // Watch for page unload to reset state
  298. window.addEventListener('beforeunload', () => {
  299. devTools.reset();
  300. });
  301. // Listen for messages from the content script
  302. window.addEventListener('message', function(event) {
  303. // Only accept messages from the same frame
  304. if (event.source !== window) return;
  305. const message = event.data;
  306. if (!message || message.id !== 'three-devtools') return;
  307. // Handle request for initial state from panel
  308. if ( message.name === 'request-state' ) {
  309. sendState();
  310. }
  311. });
  312. function sendState() {
  313. // Send current renderers
  314. for (const observedRenderer of observedRenderers) {
  315. const data = getObjectData(observedRenderer);
  316. if (data) {
  317. data.properties = getRendererProperties(observedRenderer);
  318. dispatchEvent('renderer', data);
  319. }
  320. }
  321. // Send current scenes
  322. for (const observedScene of observedScenes) {
  323. reloadSceneObjects(observedScene);
  324. }
  325. }
  326. function dispatchEvent(type, detail) {
  327. try {
  328. window.postMessage({
  329. id: 'three-devtools',
  330. type: type,
  331. detail: detail
  332. }, '*');
  333. } catch (error) {
  334. // If we get an "Extension context invalidated" error, stop all monitoring
  335. if (error.message.includes('Extension context invalidated')) {
  336. console.log('DevTools: Extension context invalidated, stopping monitoring');
  337. devTools.reset();
  338. return;
  339. }
  340. console.warn('DevTools: Error dispatching event:', error);
  341. }
  342. }
  343. function getWebGLInfo(renderer) {
  344. if (!renderer || !renderer.domElement) return null;
  345. const gl = renderer.domElement.getContext('webgl2') || renderer.domElement.getContext('webgl');
  346. if (!gl) return null;
  347. return {
  348. version: gl.getParameter(gl.VERSION),
  349. gpu: gl.getParameter(gl.RENDERER),
  350. vendor: gl.getParameter(gl.VENDOR),
  351. maxTextures: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
  352. maxAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
  353. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
  354. maxCubemapSize: gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)
  355. };
  356. }
  357. // Function to manually reload scene objects
  358. function reloadSceneObjects(scene) {
  359. const batchObjects = [];
  360. // Recursively observe all objects, collect data, update local cache
  361. function observeAndBatchObject(object) {
  362. if (!object || !object.uuid) return; // Simplified check
  363. // console.log('DevTools: Processing object during reload:', object.type || object.constructor.name, object.uuid);
  364. // Get object data
  365. const objectData = getObjectData(object);
  366. if (objectData) {
  367. batchObjects.push(objectData); // Add to batch
  368. // Update or add to local cache immediately
  369. devTools.objects.set(object.uuid, objectData);
  370. }
  371. // Process children recursively
  372. if (object.children && Array.isArray(object.children)) {
  373. // console.log('DevTools: Processing', object.children.length, 'children of', object.type || object.constructor.name);
  374. object.children.forEach(child => observeAndBatchObject(child));
  375. }
  376. }
  377. // Start traversal from the scene itself
  378. observeAndBatchObject(scene);
  379. // --- Caching Logic ---
  380. const currentObjectCount = batchObjects.length;
  381. const previousObjectCount = sceneObjectCountCache.get(scene.uuid);
  382. if (currentObjectCount !== previousObjectCount) {
  383. console.log(`DevTools: Scene ${scene.uuid} count changed (${previousObjectCount} -> ${currentObjectCount}), dispatching update.`);
  384. // Dispatch the batch update for the panel as 'scene'
  385. dispatchEvent('scene', { sceneUuid: scene.uuid, objects: batchObjects });
  386. // Update the cache
  387. sceneObjectCountCache.set(scene.uuid, currentObjectCount);
  388. } else {
  389. // console.log(`DevTools: Scene ${scene.uuid} count unchanged (${currentObjectCount}), skipping dispatch.`);
  390. }
  391. }
  392. }
粤ICP备19079148号