bridge.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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 (especially for WebGLRenderer)
  190. if (!obj.uuid) {
  191. obj.uuid = generateUUID();
  192. // console.log('DevTools: Generated UUID for object:', obj.uuid);
  193. }
  194. // Skip if already registered
  195. if (devTools.objects.has(obj.uuid)) {
  196. // console.log('DevTools: Object already registered:', obj.uuid);
  197. return;
  198. }
  199. // console.log('DevTools: Found Three.js object:', obj.type || obj.constructor.name);
  200. // Get data for this object
  201. const data = getObjectData(obj);
  202. if (data) {
  203. // console.log('DevTools: Got object data:', data);
  204. // If this is a renderer, start periodic updates
  205. if (obj.isWebGLRenderer) {
  206. // console.log('DevTools: Starting periodic updates for renderer:', obj.uuid);
  207. data.properties = getRendererProperties(obj);
  208. observedRenderers.push(obj);
  209. startRendererMonitoring(obj);
  210. }
  211. // Store the object data
  212. devTools.objects.set(obj.uuid, data);
  213. dispatchEvent('observe', data);
  214. // If this is a scene, store the reference and traverse its children
  215. if (obj.isScene) {
  216. // console.log('DevTools: Traversing scene children');
  217. // Store the scene reference locally
  218. observedScenes.push(obj);
  219. // First observe all existing children
  220. const processedObjects = new Set([obj.uuid]);
  221. function observeObject(object) {
  222. if (!processedObjects.has(object.uuid)) {
  223. processedObjects.add(object.uuid);
  224. const objectData = getObjectData(object);
  225. if (objectData) {
  226. devTools.objects.set(object.uuid, objectData);
  227. dispatchEvent('observe', objectData);
  228. }
  229. // Process children
  230. object.children.forEach(child => observeObject(child));
  231. }
  232. }
  233. // Process all children
  234. obj.children.forEach(child => observeObject(child));
  235. // Start monitoring for changes
  236. startSceneMonitoring(obj);
  237. }
  238. }
  239. });
  240. // Function to get renderer properties
  241. function getRendererProperties(renderer) {
  242. const webglInfo = getWebGLInfo(renderer);
  243. const context = renderer.getContext ? renderer.getContext() : null;
  244. const contextAttributes = context ? context.getContextAttributes() : null;
  245. return {
  246. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  247. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  248. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  249. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  250. alpha: contextAttributes ? contextAttributes.alpha : false,
  251. antialias: contextAttributes ? contextAttributes.antialias : false,
  252. autoClear: renderer.autoClear,
  253. autoClearColor: renderer.autoClearColor,
  254. autoClearDepth: renderer.autoClearDepth,
  255. autoClearStencil: renderer.autoClearStencil,
  256. localClippingEnabled: renderer.localClippingEnabled,
  257. physicallyCorrectLights: renderer.physicallyCorrectLights,
  258. outputColorSpace: renderer.outputColorSpace,
  259. toneMapping: renderer.toneMapping,
  260. toneMappingExposure: renderer.toneMappingExposure,
  261. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  262. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  263. info: {
  264. render: {
  265. frame: renderer.info.render.frame,
  266. calls: renderer.info.render.calls,
  267. triangles: renderer.info.render.triangles,
  268. points: renderer.info.render.points,
  269. lines: renderer.info.render.lines,
  270. geometries: renderer.info.render.geometries,
  271. sprites: renderer.info.render.sprites
  272. },
  273. memory: {
  274. geometries: renderer.info.memory.geometries,
  275. textures: renderer.info.memory.textures,
  276. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  277. renderLists: renderer.info.memory.renderLists,
  278. renderTargets: renderer.info.memory.renderTargets
  279. },
  280. webgl: webglInfo || {
  281. version: 'unknown',
  282. gpu: 'unknown',
  283. vendor: 'unknown',
  284. maxTextures: 'unknown',
  285. maxAttributes: 'unknown',
  286. maxTextureSize: 'unknown',
  287. maxCubemapSize: 'unknown'
  288. }
  289. }
  290. };
  291. }
  292. // Function to start renderer monitoring
  293. function startRendererMonitoring(renderer) {
  294. // Function to monitor renderer properties
  295. function monitorRendererProperties() {
  296. try {
  297. const data = devTools.objects.get( renderer.uuid );
  298. if ( ! data ) {
  299. return;
  300. }
  301. const oldProperties = data.properties;
  302. const newProperties = getRendererProperties( renderer );
  303. // Compare relevant properties directly for changes
  304. const changed = (
  305. !oldProperties || // Update if old properties don't exist yet
  306. oldProperties.width !== newProperties.width ||
  307. oldProperties.height !== newProperties.height ||
  308. oldProperties.drawingBufferWidth !== newProperties.drawingBufferWidth ||
  309. oldProperties.drawingBufferHeight !== newProperties.drawingBufferHeight ||
  310. JSON.stringify(oldProperties.info?.render) !== JSON.stringify(newProperties.info?.render) || // Compare render stats
  311. JSON.stringify(oldProperties.info?.memory) !== JSON.stringify(newProperties.info?.memory) // Compare memory stats
  312. );
  313. if ( changed ) {
  314. data.properties = newProperties;
  315. dispatchEvent( 'update', data );
  316. } else {
  317. }
  318. } catch ( error ) {
  319. // If we get an "Extension context invalidated" error, stop monitoring
  320. if ( error.message.includes( 'Extension context invalidated' ) ) {
  321. devTools.reset();
  322. return;
  323. }
  324. console.warn( 'DevTools: Error in renderer monitoring:', error );
  325. }
  326. }
  327. // TODO: Trigger monitorRendererProperties some other way, e.g., on demand or via events?
  328. }
  329. // Start periodic renderer checks
  330. // console.log('DevTools: Starting periodic renderer checks');
  331. // Function to check if bridge is available
  332. function checkBridgeAvailability() {
  333. const hasDevTools = window.hasOwnProperty('__THREE_DEVTOOLS__');
  334. const devToolsValue = window.__THREE_DEVTOOLS__;
  335. // If we have devtools and we're interactive or complete, trigger ready
  336. if (hasDevTools && devToolsValue && (document.readyState === 'interactive' || document.readyState === 'complete')) {
  337. devTools.dispatchEvent(new CustomEvent('devtools-ready'));
  338. }
  339. }
  340. // Watch for readyState changes
  341. document.addEventListener('readystatechange', () => {
  342. if (document.readyState === 'loading') {
  343. devTools.reset();
  344. }
  345. checkBridgeAvailability();
  346. });
  347. // Watch for page unload to reset state
  348. window.addEventListener('beforeunload', () => {
  349. devTools.reset();
  350. });
  351. // Listen for messages from the content script
  352. window.addEventListener('message', function(event) {
  353. // Only accept messages from the same frame
  354. if (event.source !== window) return;
  355. const message = event.data;
  356. if (!message || message.id !== 'three-devtools') return;
  357. // Handle traverse request
  358. if (message.name === 'traverse' && message.uuid) {
  359. const scene = Array.from(devTools.objects.values())
  360. .find(obj => obj.uuid === message.uuid && obj.isScene);
  361. if (scene) {
  362. console.log('DevTools: Re-traversing scene:', scene.uuid);
  363. // Find the actual scene object in the page
  364. const actualScene = findObjectByUUID(message.uuid);
  365. if (actualScene) {
  366. reloadSceneObjects(actualScene);
  367. }
  368. }
  369. }
  370. // Handle reload-scene request
  371. else if (message.name === 'reload-scene' && message.uuid) {
  372. console.log('DevTools: Received reload request for scene:', message.uuid);
  373. const actualScene = findObjectByUUID(message.uuid);
  374. if (actualScene) {
  375. reloadSceneObjects(actualScene);
  376. } else {
  377. console.warn('DevTools: Could not find scene for reload:', message.uuid);
  378. }
  379. }
  380. // Handle visibility toggle
  381. else if (message.name === 'visibility' && message.uuid !== undefined) {
  382. toggleVisibility(message.uuid, message.visible);
  383. }
  384. // Handle request for initial state from panel
  385. else if ( message.name === 'request-initial-state' ) {
  386. // console.log('DevTools: Received request-initial-state, resending objects...');
  387. // Resend all known objects to the panel
  388. devTools.objects.forEach( ( objectData ) => {
  389. dispatchEvent('observe', objectData);
  390. });
  391. // console.log('DevTools: Finished resending objects.');
  392. }
  393. });
  394. // Helper function to find a Three.js object by UUID
  395. function findObjectByUUID(uuid) {
  396. console.log('DevTools: Finding object by UUID:', uuid);
  397. // Check for scenes we've observed
  398. const sceneData = Array.from(devTools.objects.values())
  399. .find(obj => obj.uuid === uuid && obj.isScene);
  400. if (sceneData) {
  401. // For scenes accessed through observe events, they are already available
  402. // through the scene object reference passed to the observe handler
  403. for (const observedScene of observedScenes) {
  404. if (observedScene && observedScene.uuid === uuid) {
  405. console.log('DevTools: Found scene in observed scenes');
  406. return observedScene;
  407. }
  408. }
  409. }
  410. console.warn('DevTools: Could not find object with UUID:', uuid);
  411. return null;
  412. }
  413. function dispatchEvent(type, detail) {
  414. try {
  415. window.postMessage({
  416. id: 'three-devtools',
  417. type: type,
  418. detail: detail
  419. }, '*');
  420. } catch (error) {
  421. // If we get an "Extension context invalidated" error, stop all monitoring
  422. if (error.message.includes('Extension context invalidated')) {
  423. console.log('DevTools: Extension context invalidated, stopping monitoring');
  424. devTools.reset();
  425. return;
  426. }
  427. console.warn('DevTools: Error dispatching event:', error);
  428. }
  429. }
  430. function getWebGLInfo(renderer) {
  431. if (!renderer || !renderer.domElement) return null;
  432. const gl = renderer.domElement.getContext('webgl2') || renderer.domElement.getContext('webgl');
  433. if (!gl) return null;
  434. return {
  435. version: gl.getParameter(gl.VERSION),
  436. gpu: gl.getParameter(gl.RENDERER),
  437. vendor: gl.getParameter(gl.VENDOR),
  438. maxTextures: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
  439. maxAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
  440. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
  441. maxCubemapSize: gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)
  442. };
  443. }
  444. // Add visibility toggle function
  445. function toggleVisibility(uuid, visible) {
  446. // Update our local state
  447. const obj = devTools.objects.get(uuid);
  448. if (!obj) return;
  449. obj.visible = visible;
  450. console.log('DevTools: Setting visibility of', obj.type || obj.constructor.name, 'to', visible);
  451. // Find the actual Three.js object using our observed scenes
  452. if (observedScenes.length > 0) {
  453. for (const scene of observedScenes) {
  454. let found = false;
  455. scene.traverse((object) => {
  456. if (object.uuid === uuid) {
  457. object.visible = visible;
  458. // If it's a light, update its helper visibility too
  459. if (object.isLight && object.helper) {
  460. object.helper.visible = visible;
  461. }
  462. found = true;
  463. console.log('DevTools: Updated visibility in scene object');
  464. }
  465. });
  466. if (found) break;
  467. }
  468. } else {
  469. console.warn('DevTools: No observed scenes found for visibility toggle');
  470. }
  471. }
  472. // Function to start scene monitoring
  473. function startSceneMonitoring(scene) {
  474. // Keep track of known object UUIDs for this scene (excluding renderers)
  475. const knownObjectUUIDs = new Set();
  476. devTools.objects.forEach((obj, uuid) => {
  477. if (!obj.isRenderer) {
  478. knownObjectUUIDs.add(uuid);
  479. }
  480. });
  481. // TODO: Trigger scene updates some other way?
  482. }
  483. // Function to manually reload scene objects
  484. function reloadSceneObjects(scene) {
  485. console.log('DevTools: Manually reloading scene objects for scene:', scene.uuid);
  486. // Track new objects to avoid duplicates
  487. const processedObjects = new Set();
  488. // Recursively observe all objects
  489. function observeObject(object) {
  490. if (!processedObjects.has(object.uuid)) {
  491. processedObjects.add(object.uuid);
  492. console.log('DevTools: Processing object during reload:', object.type || object.constructor.name, object.uuid);
  493. // Get object data
  494. const objectData = getObjectData(object);
  495. if (objectData) {
  496. if (devTools.objects.has(object.uuid)) {
  497. // Update existing object
  498. const existingData = devTools.objects.get(object.uuid);
  499. existingData.children = objectData.children;
  500. dispatchEvent('update', existingData);
  501. } else {
  502. // Add new object
  503. devTools.objects.set(object.uuid, objectData);
  504. dispatchEvent('observe', objectData);
  505. console.log('DevTools: New object observed during reload:', object.type || object.constructor.name);
  506. }
  507. }
  508. // Process children recursively
  509. if (object.children && object.children.length > 0) {
  510. console.log('DevTools: Processing', object.children.length, 'children of', object.type || object.constructor.name);
  511. object.children.forEach(child => observeObject(child));
  512. }
  513. }
  514. }
  515. // Start with the scene itself to ensure everything is traversed
  516. observeObject(scene);
  517. console.log('DevTools: Scene reload complete. Processed', processedObjects.size, 'objects');
  518. }
  519. } else {
  520. // console.log('DevTools: Bridge already initialized');
  521. }
粤ICP备19079148号