bridge.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  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. // console.log('DevTools: Creating ThreeDevToolsTarget');
  15. }
  16. addEventListener(type, listener, options) {
  17. // console.log('DevTools: Adding listener for:', type);
  18. super.addEventListener(type, listener, options);
  19. // If this is the first listener for a type, and we have backlogged events,
  20. // check if we should process them
  21. if (type !== 'devtools-ready' && this._backlog.length > 0) {
  22. this.dispatchEvent(new CustomEvent('devtools-ready'));
  23. }
  24. }
  25. dispatchEvent(event) {
  26. console.log('DevTools: Dispatching event:', event.type);
  27. if (this._ready || event.type === 'devtools-ready') {
  28. if (event.type === 'devtools-ready') {
  29. this._ready = true;
  30. console.log('DevTools: Processing backlog:', this._backlog.length, 'events');
  31. const backlog = this._backlog;
  32. this._backlog = [];
  33. backlog.forEach(e => super.dispatchEvent(e));
  34. }
  35. return super.dispatchEvent(event);
  36. } else {
  37. console.log('DevTools: Backlogging event:', event.type);
  38. this._backlog.push(event);
  39. return false; // Return false to indicate synchronous handling
  40. }
  41. }
  42. reset() {
  43. console.log('DevTools: Resetting state');
  44. // Clear all monitoring intervals
  45. this.objects.forEach((obj, uuid) => {
  46. if (obj.isRenderer || obj.isScene) {
  47. const interval = monitoringIntervals.get(obj);
  48. if (interval) {
  49. clearInterval(interval);
  50. monitoringIntervals.delete(obj);
  51. }
  52. }
  53. });
  54. // Clear objects map
  55. this.objects.clear();
  56. // Clear backlog
  57. this._backlog = [];
  58. // Reset ready state
  59. this._ready = false;
  60. // Clear observed arrays
  61. observedScenes.length = 0;
  62. observedRenderers.length = 0;
  63. }
  64. }
  65. // Create and expose the __THREE_DEVTOOLS__ object
  66. const devTools = new DevToolsEventTarget();
  67. Object.defineProperty(window, '__THREE_DEVTOOLS__', {
  68. value: devTools,
  69. configurable: false,
  70. enumerable: true,
  71. writable: false
  72. });
  73. // Store monitoring intervals without polluting objects
  74. const monitoringIntervals = new WeakMap();
  75. // Declare arrays for tracking observed objects
  76. const observedScenes = [];
  77. const observedRenderers = [];
  78. // Function to get renderer data
  79. function getRendererData(renderer) {
  80. try {
  81. const webglInfo = getWebGLInfo(renderer);
  82. const data = {
  83. uuid: renderer.uuid || generateUUID(),
  84. type: 'WebGLRenderer',
  85. name: '',
  86. visible: true,
  87. isScene: false,
  88. isObject3D: false,
  89. isCamera: false,
  90. isLight: false,
  91. isMesh: false,
  92. isRenderer: true,
  93. parent: null,
  94. children: [],
  95. // Add renderer-specific properties
  96. properties: {
  97. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  98. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  99. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  100. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  101. alpha: renderer.alpha || false,
  102. antialias: renderer.antialias || false,
  103. autoClear: renderer.autoClear,
  104. autoClearColor: renderer.autoClearColor,
  105. autoClearDepth: renderer.autoClearDepth,
  106. autoClearStencil: renderer.autoClearStencil,
  107. localClippingEnabled: renderer.localClippingEnabled,
  108. physicallyCorrectLights: renderer.physicallyCorrectLights,
  109. outputColorSpace: renderer.outputColorSpace,
  110. toneMapping: renderer.toneMapping,
  111. toneMappingExposure: renderer.toneMappingExposure,
  112. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  113. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  114. // Get current info values
  115. info: {
  116. render: {
  117. frame: renderer.info.render.frame,
  118. calls: renderer.info.render.calls,
  119. triangles: renderer.info.render.triangles,
  120. points: renderer.info.render.points,
  121. lines: renderer.info.render.lines,
  122. geometries: renderer.info.render.geometries,
  123. sprites: renderer.info.render.sprites
  124. },
  125. memory: {
  126. geometries: renderer.info.memory.geometries,
  127. textures: renderer.info.memory.textures,
  128. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  129. renderLists: renderer.info.memory.renderLists,
  130. renderTargets: renderer.info.memory.renderTargets
  131. },
  132. webgl: webglInfo || {
  133. version: 'unknown',
  134. gpu: 'unknown',
  135. vendor: 'unknown',
  136. maxTextures: 'unknown',
  137. maxAttributes: 'unknown',
  138. maxTextureSize: 'unknown',
  139. maxCubemapSize: 'unknown'
  140. }
  141. }
  142. }
  143. };
  144. return data;
  145. } catch (error) {
  146. console.warn('DevTools: Error getting renderer data:', error);
  147. return null;
  148. }
  149. }
  150. // Function to get object hierarchy
  151. function getObjectData(obj) {
  152. try {
  153. // Special case for WebGLRenderer
  154. if (obj.isWebGLRenderer === true) {
  155. return getRendererData(obj);
  156. }
  157. // Get descriptive name for the object
  158. let descriptiveName;
  159. if (obj.isMesh) {
  160. const geoType = obj.geometry ? obj.geometry.type : 'Unknown';
  161. const matType = obj.material ?
  162. (Array.isArray(obj.material) ?
  163. obj.material.map(m => m.type).join(', ') :
  164. obj.material.type) :
  165. 'Unknown';
  166. descriptiveName = `${obj.type || 'Mesh'} <span class="object-details">${geoType} ${matType}</span>`;
  167. } else if (obj.isLight) {
  168. descriptiveName = `${obj.type || 'Light'}`;
  169. } else if (obj.isCamera) {
  170. descriptiveName = `${obj.type || 'Camera'}`;
  171. } else {
  172. descriptiveName = obj.type || obj.constructor.name;
  173. }
  174. const data = {
  175. uuid: obj.uuid,
  176. type: obj.type || obj.constructor.name,
  177. name: descriptiveName,
  178. visible: obj.visible !== undefined ? obj.visible : true,
  179. isScene: obj.isScene === true,
  180. isObject3D: obj.isObject3D === true,
  181. isCamera: obj.isCamera === true,
  182. isLight: obj.isLight === true,
  183. isMesh: obj.isMesh === true,
  184. isRenderer: obj.isWebGLRenderer === true,
  185. parent: obj.parent ? obj.parent.uuid : null,
  186. children: obj.children ? obj.children.map(child => child.uuid) : []
  187. };
  188. // console.log('DevTools: Object data:', data);
  189. return data;
  190. } catch (error) {
  191. console.warn('DevTools: Error getting object data:', error);
  192. return null;
  193. }
  194. }
  195. // Generate a UUID for objects that don't have one
  196. function generateUUID() {
  197. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  198. const r = Math.random() * 16 | 0;
  199. const v = c === 'x' ? r : (r & 0x3 | 0x8);
  200. return v.toString(16);
  201. });
  202. }
  203. // Listen for Three.js registration
  204. devTools.addEventListener('register', (event) => {
  205. console.log('DevTools: Three.js registered with revision:', event.detail.revision);
  206. dispatchEvent('register', event.detail);
  207. });
  208. // Listen for object observations
  209. devTools.addEventListener('observe', (event) => {
  210. const obj = event.detail;
  211. if (!obj) {
  212. console.warn('DevTools: Received observe event with null/undefined detail');
  213. return;
  214. }
  215. console.log('DevTools: Received object:', {
  216. type: obj.type || obj.constructor.name,
  217. isWebGLRenderer: obj.isWebGLRenderer === true,
  218. hasUUID: !!obj.uuid
  219. });
  220. // Generate UUID if needed (especially for WebGLRenderer)
  221. if (!obj.uuid) {
  222. obj.uuid = generateUUID();
  223. console.log('DevTools: Generated UUID for object:', obj.uuid);
  224. }
  225. // Skip if already registered
  226. if (devTools.objects.has(obj.uuid)) {
  227. console.log('DevTools: Object already registered:', obj.uuid);
  228. return;
  229. }
  230. console.log('DevTools: Found Three.js object:', obj.type || obj.constructor.name);
  231. // Get data for this object
  232. const data = getObjectData(obj);
  233. if (data) {
  234. console.log('DevTools: Got object data:', data);
  235. // If this is a renderer, start periodic updates
  236. if (obj.isWebGLRenderer) {
  237. console.log('DevTools: Starting periodic updates for renderer:', obj.uuid);
  238. data.properties = getRendererProperties(obj);
  239. observedRenderers.push(obj);
  240. startRendererMonitoring(obj);
  241. }
  242. // Store the object data
  243. devTools.objects.set(obj.uuid, data);
  244. dispatchEvent('observe', data);
  245. // If this is a scene, store the reference and traverse its children
  246. if (obj.isScene) {
  247. console.log('DevTools: Traversing scene children');
  248. // Store the scene reference locally
  249. observedScenes.push(obj);
  250. // First observe all existing children
  251. const processedObjects = new Set([obj.uuid]);
  252. function observeObject(object) {
  253. if (!processedObjects.has(object.uuid)) {
  254. processedObjects.add(object.uuid);
  255. const objectData = getObjectData(object);
  256. if (objectData) {
  257. devTools.objects.set(object.uuid, objectData);
  258. dispatchEvent('observe', objectData);
  259. }
  260. // Process children
  261. object.children.forEach(child => observeObject(child));
  262. }
  263. }
  264. // Process all children
  265. obj.children.forEach(child => observeObject(child));
  266. // Start monitoring for changes
  267. startSceneMonitoring(obj);
  268. }
  269. }
  270. });
  271. // Function to get renderer properties
  272. function getRendererProperties(renderer) {
  273. const webglInfo = getWebGLInfo(renderer);
  274. return {
  275. width: renderer.domElement ? renderer.domElement.clientWidth : 0,
  276. height: renderer.domElement ? renderer.domElement.clientHeight : 0,
  277. drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
  278. drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
  279. alpha: renderer.alpha || false,
  280. antialias: renderer.antialias || false,
  281. autoClear: renderer.autoClear,
  282. autoClearColor: renderer.autoClearColor,
  283. autoClearDepth: renderer.autoClearDepth,
  284. autoClearStencil: renderer.autoClearStencil,
  285. localClippingEnabled: renderer.localClippingEnabled,
  286. physicallyCorrectLights: renderer.physicallyCorrectLights,
  287. outputColorSpace: renderer.outputColorSpace,
  288. toneMapping: renderer.toneMapping,
  289. toneMappingExposure: renderer.toneMappingExposure,
  290. shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
  291. shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
  292. info: {
  293. render: {
  294. frame: renderer.info.render.frame,
  295. calls: renderer.info.render.calls,
  296. triangles: renderer.info.render.triangles,
  297. points: renderer.info.render.points,
  298. lines: renderer.info.render.lines,
  299. geometries: renderer.info.render.geometries,
  300. sprites: renderer.info.render.sprites
  301. },
  302. memory: {
  303. geometries: renderer.info.memory.geometries,
  304. textures: renderer.info.memory.textures,
  305. programs: renderer.info.programs ? renderer.info.programs.length : 0,
  306. renderLists: renderer.info.memory.renderLists,
  307. renderTargets: renderer.info.memory.renderTargets
  308. },
  309. webgl: webglInfo || {
  310. version: 'unknown',
  311. gpu: 'unknown',
  312. vendor: 'unknown',
  313. maxTextures: 'unknown',
  314. maxAttributes: 'unknown',
  315. maxTextureSize: 'unknown',
  316. maxCubemapSize: 'unknown'
  317. }
  318. }
  319. };
  320. }
  321. // Function to start renderer monitoring
  322. function startRendererMonitoring(renderer) {
  323. // Clear any existing monitoring
  324. const existingInterval = monitoringIntervals.get(renderer);
  325. if (existingInterval) {
  326. clearInterval(existingInterval);
  327. }
  328. // Function to monitor renderer properties
  329. function monitorRendererProperties() {
  330. try {
  331. // Skip updates if devtools is not visible
  332. if ( ! devTools.isVisible ) {
  333. return;
  334. }
  335. const data = devTools.objects.get( renderer.uuid );
  336. if ( ! data ) {
  337. clearInterval( intervalId );
  338. monitoringIntervals.delete( renderer );
  339. return;
  340. }
  341. const newProperties = getRendererProperties( renderer );
  342. if ( JSON.stringify( data.properties ) !== JSON.stringify( newProperties ) ) {
  343. data.properties = newProperties;
  344. dispatchEvent( 'update', data );
  345. }
  346. } catch ( error ) {
  347. // If we get an "Extension context invalidated" error, stop monitoring
  348. if ( error.message.includes( 'Extension context invalidated' ) ) {
  349. clearInterval( intervalId );
  350. monitoringIntervals.delete( renderer );
  351. devTools.reset();
  352. return;
  353. }
  354. console.warn( 'DevTools: Error in renderer monitoring:', error );
  355. }
  356. }
  357. const intervalId = setInterval(monitorRendererProperties, 1000);
  358. monitoringIntervals.set(renderer, intervalId);
  359. }
  360. // Start periodic renderer checks
  361. console.log('DevTools: Starting periodic renderer checks');
  362. // Function to check if bridge is available
  363. function checkBridgeAvailability() {
  364. const hasDevTools = window.hasOwnProperty('__THREE_DEVTOOLS__');
  365. const devToolsValue = window.__THREE_DEVTOOLS__;
  366. // If we have devtools and we're interactive or complete, trigger ready
  367. if (hasDevTools && devToolsValue && (document.readyState === 'interactive' || document.readyState === 'complete')) {
  368. devTools.dispatchEvent(new CustomEvent('devtools-ready'));
  369. }
  370. }
  371. // Watch for readyState changes
  372. document.addEventListener('readystatechange', () => {
  373. // console.log('DevTools: Document readyState changed to:', document.readyState);
  374. if (document.readyState === 'loading') {
  375. devTools.reset();
  376. }
  377. checkBridgeAvailability();
  378. });
  379. // Watch for page unload to reset state
  380. window.addEventListener('beforeunload', () => {
  381. devTools.reset();
  382. });
  383. // Listen for messages from the content script
  384. window.addEventListener('message', function(event) {
  385. // Only accept messages from the same frame
  386. if (event.source !== window) return;
  387. const message = event.data;
  388. if (!message || message.id !== 'three-devtools') return;
  389. // Handle traverse request
  390. if (message.name === 'traverse' && message.uuid) {
  391. // Find the scene in our objects
  392. const scene = Array.from(devTools.objects.values())
  393. .find(obj => obj.uuid === message.uuid && obj.isScene);
  394. if (scene) {
  395. console.log('DevTools: Re-traversing scene:', scene.uuid);
  396. // Find the actual scene object in the page
  397. const actualScene = findObjectByUUID(message.uuid);
  398. if (actualScene) {
  399. reloadSceneObjects(actualScene);
  400. }
  401. }
  402. }
  403. // Handle reload-scene request
  404. else if (message.name === 'reload-scene' && message.uuid) {
  405. console.log('DevTools: Received reload request for scene:', message.uuid);
  406. const actualScene = findObjectByUUID(message.uuid);
  407. if (actualScene) {
  408. reloadSceneObjects(actualScene);
  409. } else {
  410. console.warn('DevTools: Could not find scene for reload:', message.uuid);
  411. }
  412. }
  413. // Handle visibility toggle
  414. else if (message.name === 'visibility' && message.uuid !== undefined) {
  415. toggleVisibility(message.uuid, message.visible);
  416. }
  417. });
  418. // Helper function to find a Three.js object by UUID
  419. function findObjectByUUID(uuid) {
  420. console.log('DevTools: Finding object by UUID:', uuid);
  421. // Check for scenes we've observed
  422. const sceneData = Array.from(devTools.objects.values())
  423. .find(obj => obj.uuid === uuid && obj.isScene);
  424. if (sceneData) {
  425. // For scenes accessed through observe events, they are already available
  426. // through the scene object reference passed to the observe handler
  427. for (const observedScene of observedScenes) {
  428. if (observedScene && observedScene.uuid === uuid) {
  429. console.log('DevTools: Found scene in observed scenes');
  430. return observedScene;
  431. }
  432. }
  433. }
  434. console.warn('DevTools: Could not find object with UUID:', uuid);
  435. return null;
  436. }
  437. function dispatchEvent(type, detail) {
  438. try {
  439. window.postMessage({
  440. id: 'three-devtools',
  441. type: type,
  442. detail: detail
  443. }, '*');
  444. } catch (error) {
  445. // If we get an "Extension context invalidated" error, stop all monitoring
  446. if (error.message.includes('Extension context invalidated')) {
  447. console.log('DevTools: Extension context invalidated, stopping monitoring');
  448. devTools.reset();
  449. return;
  450. }
  451. console.warn('DevTools: Error dispatching event:', error);
  452. }
  453. }
  454. function getWebGLInfo(renderer) {
  455. if (!renderer || !renderer.domElement) return null;
  456. const gl = renderer.domElement.getContext('webgl2') || renderer.domElement.getContext('webgl');
  457. if (!gl) return null;
  458. return {
  459. version: gl.getParameter(gl.VERSION),
  460. gpu: gl.getParameter(gl.RENDERER),
  461. vendor: gl.getParameter(gl.VENDOR),
  462. maxTextures: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
  463. maxAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
  464. maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
  465. maxCubemapSize: gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)
  466. };
  467. }
  468. // Add visibility toggle function
  469. function toggleVisibility(uuid, visible) {
  470. // Update our local state
  471. const obj = devTools.objects.get(uuid);
  472. if (!obj) return;
  473. obj.visible = visible;
  474. console.log('DevTools: Setting visibility of', obj.type || obj.constructor.name, 'to', visible);
  475. // Find the actual Three.js object using our observed scenes
  476. if (observedScenes.length > 0) {
  477. for (const scene of observedScenes) {
  478. let found = false;
  479. scene.traverse((object) => {
  480. if (object.uuid === uuid) {
  481. object.visible = visible;
  482. // If it's a light, update its helper visibility too
  483. if (object.isLight && object.helper) {
  484. object.helper.visible = visible;
  485. }
  486. found = true;
  487. console.log('DevTools: Updated visibility in scene object');
  488. }
  489. });
  490. if (found) break;
  491. }
  492. } else {
  493. console.warn('DevTools: No observed scenes found for visibility toggle');
  494. }
  495. }
  496. // Function to start scene monitoring
  497. function startSceneMonitoring(scene) {
  498. // Clear any existing monitoring
  499. const existingInterval = monitoringIntervals.get(scene);
  500. if (existingInterval) {
  501. clearInterval(existingInterval);
  502. }
  503. // Set up monitoring interval
  504. const intervalId = setInterval(() => {
  505. try {
  506. // Clear existing objects except renderers and the scene itself
  507. devTools.objects.forEach((obj, uuid) => {
  508. if (!obj.isRenderer && uuid !== scene.uuid) {
  509. devTools.objects.delete(uuid);
  510. dispatchEvent('remove', { uuid });
  511. }
  512. });
  513. // Traverse and recreate the entire object list
  514. function traverseScene(object) {
  515. const objectData = getObjectData(object);
  516. if (objectData) {
  517. devTools.objects.set(object.uuid, objectData);
  518. dispatchEvent('observe', objectData);
  519. // Traverse children
  520. object.children.forEach(child => traverseScene(child));
  521. }
  522. }
  523. // Start traversal from scene root
  524. traverseScene(scene);
  525. } catch (error) {
  526. // If we get an "Extension context invalidated" error, stop monitoring
  527. if (error.message.includes('Extension context invalidated')) {
  528. clearInterval(intervalId);
  529. monitoringIntervals.delete(scene);
  530. devTools.reset();
  531. return;
  532. }
  533. console.warn('DevTools: Error in scene monitoring:', error);
  534. }
  535. }, 1000);
  536. monitoringIntervals.set(scene, intervalId);
  537. // Clean up monitoring when scene is disposed
  538. const originalDispose = scene.dispose;
  539. scene.dispose = function() {
  540. const intervalId = monitoringIntervals.get(this);
  541. if (intervalId) {
  542. clearInterval(intervalId);
  543. monitoringIntervals.delete(this);
  544. }
  545. if (originalDispose) {
  546. originalDispose.call(this);
  547. }
  548. };
  549. }
  550. // Function to manually reload scene objects
  551. function reloadSceneObjects(scene) {
  552. console.log('DevTools: Manually reloading scene objects for scene:', scene.uuid);
  553. // Track new objects to avoid duplicates
  554. const processedObjects = new Set();
  555. // Recursively observe all objects
  556. function observeObject(object) {
  557. if (!processedObjects.has(object.uuid)) {
  558. processedObjects.add(object.uuid);
  559. console.log('DevTools: Processing object during reload:', object.type || object.constructor.name, object.uuid);
  560. // Get object data
  561. const objectData = getObjectData(object);
  562. if (objectData) {
  563. if (devTools.objects.has(object.uuid)) {
  564. // Update existing object
  565. const existingData = devTools.objects.get(object.uuid);
  566. existingData.children = objectData.children;
  567. dispatchEvent('update', existingData);
  568. } else {
  569. // Add new object
  570. devTools.objects.set(object.uuid, objectData);
  571. dispatchEvent('observe', objectData);
  572. console.log('DevTools: New object observed during reload:', object.type || object.constructor.name);
  573. }
  574. }
  575. // Process children recursively
  576. if (object.children && object.children.length > 0) {
  577. console.log('DevTools: Processing', object.children.length, 'children of', object.type || object.constructor.name);
  578. object.children.forEach(child => observeObject(child));
  579. }
  580. }
  581. }
  582. // Start with the scene itself to ensure everything is traversed
  583. observeObject(scene);
  584. console.log('DevTools: Scene reload complete. Processed', processedObjects.size, 'objects');
  585. }
  586. } else {
  587. console.log('DevTools: Bridge already initialized');
  588. }
粤ICP备19079148号