Agent.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. import * as Commands from './commands/Commands.js';
  2. import { Vector3, BoxGeometry, SphereGeometry, MeshStandardMaterial, Mesh, DirectionalLight, PointLight, AmbientLight, Color, CylinderGeometry } from 'three';
  3. class Agent {
  4. constructor( editor ) {
  5. this.editor = editor;
  6. this.container = new THREE.Group();
  7. this.dom = document.createElement( 'div' );
  8. this.dom.id = 'agent';
  9. this.lastModifiedObject = null; // Track last modified object
  10. // Create UI elements
  11. this.createUI();
  12. // Initialize signals
  13. this.signals = {
  14. agentResponse: new signals.Signal(),
  15. agentThinking: new signals.Signal()
  16. };
  17. // Bind methods
  18. this.processQuery = this.processQuery.bind( this );
  19. this.executeCommand = this.executeCommand.bind( this );
  20. this.generateRandomColor = this.generateRandomColor.bind( this );
  21. this.generateUniqueObjectName = this.generateUniqueObjectName.bind( this );
  22. this.editor.signals.storageLoaded.add( () => {
  23. this.init();
  24. } );
  25. }
  26. generateUniqueObjectName( baseName ) {
  27. const scene = this.editor.scene;
  28. let counter = 1;
  29. let name;
  30. // Keep incrementing counter until we find an unused name
  31. do {
  32. name = `${baseName}${counter}`;
  33. counter ++;
  34. } while ( scene.getObjectByName( name ) !== undefined );
  35. return name;
  36. }
  37. generateRandomColor() {
  38. const randomHex = Math.floor( Math.random() * 16777215 ).toString( 16 );
  39. return '#' + randomHex.padStart( 6, '0' );
  40. }
  41. createUI() {
  42. // Create message bubble
  43. const messageBubble = document.createElement( 'div' );
  44. messageBubble.style.display = 'none';
  45. messageBubble.style.padding = '8px 12px';
  46. messageBubble.style.borderRadius = '4px';
  47. messageBubble.style.marginBottom = '8px';
  48. messageBubble.style.fontSize = '14px';
  49. messageBubble.style.position = 'relative';
  50. messageBubble.style.maxHeight = '400px'; // Add max height
  51. messageBubble.style.overflowY = 'auto'; // Add vertical scrollbar when needed
  52. // Add message container first
  53. const messageContainer = document.createElement( 'div' );
  54. messageContainer.className = 'message-text';
  55. messageContainer.style.marginRight = '20px'; // Make space for the close button
  56. messageContainer.style.whiteSpace = 'pre-wrap';
  57. messageBubble.appendChild( messageContainer );
  58. // Add close button
  59. const closeButton = document.createElement( 'div' );
  60. closeButton.innerHTML = '×';
  61. closeButton.style.position = 'absolute';
  62. closeButton.style.top = '8px';
  63. closeButton.style.right = '8px';
  64. closeButton.style.cursor = 'pointer';
  65. closeButton.style.fontSize = '18px';
  66. closeButton.style.lineHeight = '14px';
  67. closeButton.style.zIndex = '1';
  68. closeButton.onclick = () => {
  69. messageBubble.style.display = 'none';
  70. };
  71. messageBubble.appendChild( closeButton );
  72. // Create input area
  73. const input = document.createElement( 'textarea' );
  74. input.placeholder = 'What do you want to do?';
  75. // Prevent keyboard shortcuts when focused
  76. input.addEventListener( 'keydown', ( e ) => {
  77. e.stopPropagation();
  78. if ( e.key === 'Enter' ) {
  79. if ( e.shiftKey ) {
  80. // Allow Shift+Enter for newlines
  81. return;
  82. }
  83. e.preventDefault();
  84. executeQuery();
  85. }
  86. } );
  87. // Create submit button
  88. const button = document.createElement( 'button' );
  89. button.textContent = 'SEND';
  90. const executeQuery = async () => {
  91. if ( button.disabled || ! input.value.trim() ) return;
  92. button.disabled = true;
  93. input.disabled = true;
  94. await this.processQuery( input.value );
  95. input.value = '';
  96. button.disabled = false;
  97. input.disabled = false;
  98. input.focus();
  99. };
  100. // Add event listeners
  101. button.addEventListener( 'click', executeQuery );
  102. // Append elements
  103. this.dom.appendChild( messageBubble );
  104. this.dom.appendChild( input );
  105. this.dom.appendChild( button );
  106. // Store references
  107. this.messageBubble = messageBubble;
  108. }
  109. showError( message ) {
  110. this.showMessage( message, 'error' );
  111. }
  112. showMessage( message, type = 'normal' ) {
  113. // Get the message container
  114. const messageContainer = this.messageBubble.querySelector( '.message-text' );
  115. const closeButton = this.messageBubble.querySelector( 'div:last-child' );
  116. // Set styles based on message type
  117. if ( type === 'error' ) {
  118. this.messageBubble.style.backgroundColor = '#ffebee';
  119. this.messageBubble.style.color = '#d32f2f';
  120. closeButton.style.color = '#d32f2f';
  121. } else {
  122. this.messageBubble.style.backgroundColor = '#e8f5e9';
  123. this.messageBubble.style.color = '#2e7d32';
  124. closeButton.style.color = '#2e7d32';
  125. }
  126. // Update message text
  127. messageContainer.textContent = message;
  128. this.messageBubble.style.display = 'block';
  129. }
  130. async init() {
  131. // Initialize Google AI
  132. // const session = new GoogleGenAI( { apiKey: 'GENAI_API_KEY' } );
  133. // Get scene information
  134. const sceneInfo = this.getSceneInfo();
  135. // Prepare prompt
  136. const systemPrompt = `You are a Three.js scene manipulation assistant. Current scene info:
  137. ${JSON.stringify( sceneInfo, null, 2 )}
  138. Available commands:
  139. - AddObject: Add a new object to the scene
  140. Types: box/cube, sphere, directionalLight, pointLight, ambientLight, cylinder
  141. Box parameters:
  142. - width, height, depth (default: 1)
  143. - widthSegments, heightSegments, depthSegments (default: 1) - controls geometry detail
  144. Sphere parameters:
  145. - radius (default: 0.5)
  146. - widthSegments (default: 32) - horizontal detail
  147. - heightSegments (default: 16) - vertical detail
  148. Cylinder parameters:
  149. - radiusTop (default: 0.5)
  150. - radiusBottom (default: 0.5)
  151. - height (default: 1)
  152. - radialSegments (default: 32) - horizontal detail
  153. - heightSegments (default: 1) - vertical detail
  154. - openEnded (default: false)
  155. DirectionalLight parameters:
  156. - color (default: white)
  157. - intensity (default: 1)
  158. PointLight parameters:
  159. - color (default: white)
  160. - intensity (default: 1)
  161. - distance (default: 0)
  162. - decay (default: 2)
  163. AmbientLight parameters:
  164. - color (default: white)
  165. - intensity (default: 1)
  166. Common parameters for all:
  167. - color (use simple color names like "red" or hex values like "#ff0000" - do not use functions or dynamic values)
  168. - position (e.g. {x: 0, y: 5, z: 0})
  169. - SetPosition: Set object position
  170. Parameters:
  171. - object: name of the object to move (optional - defaults to last modified object)
  172. - position: {x, y, z} (omitted coordinates keep current values)
  173. Example: Move right = {x: 2}, Move up = {y: 2}
  174. - SetMaterialColor: Change object material color
  175. Parameters:
  176. - object: name of the object (optional - defaults to last modified object)
  177. - color: color value (e.g. "red", "#ff0000", or "random" for a random color)
  178. Note: Use "random" keyword for random colors, do not use JavaScript expressions
  179. - SetScale: Change object size
  180. Parameters:
  181. - object: name of the object (optional - defaults to last modified object)
  182. - scale: {x, y, z} (values > 1 make bigger, < 1 make smaller)
  183. Example: Double size = {x: 2, y: 2, z: 2}
  184. Example: Half size = {x: 0.5, y: 0.5, z: 0.5}
  185. - SetMaterialValue: Set material property value
  186. Parameters:
  187. - object: name of the object (optional - defaults to last modified object)
  188. - property: material property to set (e.g. "metalness", "roughness", "wireframe", "transparent", "opacity")
  189. - value: value to set (numbers between 0-1 for metalness/roughness/opacity, true/false for wireframe/transparent)
  190. Example: Make metallic = { property: "metalness", value: 1.0 }
  191. Example: Make rough = { property: "roughness", value: 1.0 }
  192. Example: Make reflective = Use MultiCmds to set both metalness=1.0 and roughness=0.0
  193. Example: Make transparent = { property: "transparent", value: true, opacity: 0.5 }
  194. Note: For reflective surfaces, combine metalness=1.0 with roughness=0.0 using MultiCmds
  195. - SetRotation: Set object rotation
  196. Parameters:
  197. - object: name of the object (optional - defaults to last modified object)
  198. - rotation: {x, y, z} in radians
  199. - SetGeometry: Modify object geometry detail
  200. Parameters:
  201. - object: name of the object to modify (optional - defaults to last modified object)
  202. - widthSegments: number of segments along width (for box/sphere)
  203. - heightSegments: number of segments along height (for box/sphere)
  204. - depthSegments: number of segments along depth (for box only)
  205. Example: High detail sphere = { widthSegments: 64, heightSegments: 32 }
  206. Example: High detail box = { widthSegments: 4, heightSegments: 4, depthSegments: 4 }
  207. - RemoveObject: Remove an object from the scene
  208. Parameters:
  209. - object: name of the object to remove
  210. - MultiCmds: Execute multiple commands in sequence
  211. Parameters:
  212. - commands: array of command objects
  213. Example - Create multiple objects:
  214. {
  215. "type": "MultiCmds",
  216. "params": {
  217. "commands": [
  218. {
  219. "type": "AddObject",
  220. "params": {
  221. "type": "cube",
  222. "name": "Cube1",
  223. "position": {"x": -1.5}
  224. }
  225. },
  226. {
  227. "type": "AddObject",
  228. "params": {
  229. "type": "cube",
  230. "name": "Cube2",
  231. "position": {"x": -0.5}
  232. }
  233. },
  234. {
  235. "type": "AddObject",
  236. "params": {
  237. "type": "cube",
  238. "name": "Cube3",
  239. "position": {"x": 0.5}
  240. }
  241. },
  242. {
  243. "type": "AddObject",
  244. "params": {
  245. "type": "cube",
  246. "name": "Cube4",
  247. "position": {"x": 1.5}
  248. }
  249. }
  250. ]
  251. }
  252. }
  253. Example - Create and modify an object:
  254. {
  255. "type": "MultiCmds",
  256. "params": {
  257. "commands": [
  258. {
  259. "type": "AddObject",
  260. "params": { "type": "cube", "name": "MyCube" }
  261. },
  262. {
  263. "type": "SetMaterialColor",
  264. "params": { "object": "MyCube", "color": "red" }
  265. },
  266. {
  267. "type": "SetScale",
  268. "params": { "object": "MyCube", "scale": {"x": 2, "y": 2, "z": 2} }
  269. }
  270. ]
  271. }
  272. }
  273. Example - Modify all objects in the scene:
  274. {
  275. "type": "MultiCmds",
  276. "params": {
  277. "commands": [
  278. {
  279. "type": "SetMaterialColor",
  280. "params": { "object": "Box1", "color": "red" }
  281. },
  282. {
  283. "type": "SetMaterialColor",
  284. "params": { "object": "Box2", "color": "blue" }
  285. }
  286. ]
  287. }
  288. }
  289. Note: Use MultiCmds when you need to:
  290. 1. Create multiple objects at once
  291. 2. Apply multiple modifications to a single object
  292. 3. Apply modifications to multiple objects
  293. 4. Any combination of the above
  294. Important: When working with multiple similar objects (e.g. multiple spheres):
  295. - Objects are automatically numbered (e.g. "Sphere1", "Sphere2", etc.)
  296. - Use the exact object name including the number when targeting specific objects
  297. - To modify all objects of a type, create a MultiCmds command with one command per object
  298. - The scene info includes:
  299. - objectCounts: how many of each type exist
  300. - objectsByType: groups of objects by their base name
  301. - spheres: list of all sphere names
  302. - boxes: list of all box names
  303. - cylinders: list of all cylinder names
  304. - directionalLights: list of all directional light names
  305. - pointLights: list of all point light names
  306. - ambientLights: list of all ambient light names
  307. Example - Set random colors for all spheres:
  308. {
  309. "type": "MultiCmds",
  310. "params": {
  311. "commands": [
  312. {
  313. "type": "SetMaterialColor",
  314. "params": { "object": "Sphere1", "color": "random" }
  315. },
  316. {
  317. "type": "SetMaterialColor",
  318. "params": { "object": "Sphere2", "color": "random" }
  319. }
  320. ]
  321. }
  322. }
  323. Respond ONLY with a JSON object in this format:
  324. {
  325. "response": "Your text response to the user explaining what you're doing",
  326. "commands": {
  327. "type": "command_type",
  328. "params": {
  329. // command specific parameters
  330. }
  331. }
  332. }
  333. Important:
  334. 1. If no commands are needed, set "commands" to null
  335. 2. Do not include any JavaScript expressions or functions in the JSON
  336. 3. For random colors, use the "random" keyword instead of Math.random()
  337. 4. Do not include any other text outside the JSON
  338. Do not include any other text outside the JSON.`;
  339. // const { available, defaultTemperature, defaultTopK, maxTopK } = await ai.languageModel.capabilities();
  340. // console.log( 'Capabilities:', available, defaultTemperature, defaultTopK, maxTopK );
  341. // if ( available !== 'readily' ) {
  342. const session = await ai.languageModel.create({
  343. systemPrompt: systemPrompt
  344. });
  345. console.log( 'SESSION:', session );
  346. this.session = session;
  347. }
  348. async processQuery( query ) {
  349. if ( ! query.trim() ) return;
  350. try {
  351. this.signals.agentThinking.dispatch();
  352. const response = await this.session.prompt( query );
  353. let responseData;
  354. try {
  355. // Strip markdown code block markers if present
  356. const cleanText = response.replace( /^```json\n|\n```$/g, '' )
  357. .replace( /^\s*```\s*|\s*```\s*$/g, '' ) // Remove any remaining code block markers
  358. .trim();
  359. try {
  360. // First try parsing as is
  361. responseData = JSON.parse( cleanText );
  362. } catch ( e ) {
  363. console.log( 'ERROR:', e );
  364. // If that fails, try to fix common JSON issues
  365. const fixedText = cleanText
  366. .replace( /,\s*([}\]])/g, '$1' ) // Remove trailing commas
  367. .replace( /([a-zA-Z0-9])\s*:\s*/g, '"$1": ' ) // Quote unquoted keys
  368. .replace( /\n/g, ' ' ) // Remove newlines
  369. .replace( /\s+/g, ' ' ); // Normalize whitespace
  370. responseData = JSON.parse( fixedText );
  371. }
  372. } catch ( e ) {
  373. console.log( 'ERROR:', e );
  374. // console.error( 'AGENT: Failed to parse AI response as JSON:', e );
  375. // console.error( 'AGENT: Raw response:', response );
  376. this.showError( response );
  377. return;
  378. }
  379. // Execute commands if present
  380. if ( responseData.commands ) {
  381. try {
  382. await this.executeCommand( responseData.commands );
  383. } catch ( e ) {
  384. console.error( 'AGENT: Failed to execute commands:', e );
  385. this.showError( 'Failed to execute command: ' + e.message );
  386. return;
  387. }
  388. }
  389. // Log the response
  390. // console.log( 'AGENT:', responseData.response );
  391. this.signals.agentResponse.dispatch( responseData.response );
  392. this.showMessage( responseData.response );
  393. } catch ( error ) {
  394. console.error( 'AGENT: Agent error:', error );
  395. this.showError( 'Agent error: ' + error.message );
  396. }
  397. console.log( this.session.tokensSoFar, '/', this.session.tokensLeft );
  398. }
  399. async executeCommand( commandData ) {
  400. if ( ! commandData.type || ! Commands[ commandData.type + 'Command' ] ) {
  401. console.error( 'AGENT: Invalid command type:', commandData.type );
  402. return;
  403. }
  404. let command;
  405. // Helper to get target object, falling back to last modified
  406. const getTargetObject = ( objectName ) => {
  407. if ( objectName ) {
  408. const object = this.editor.scene.getObjectByName( objectName );
  409. if ( object ) {
  410. this.lastModifiedObject = object;
  411. return object;
  412. }
  413. }
  414. return this.lastModifiedObject;
  415. };
  416. const createMaterial = ( params ) => {
  417. const material = new MeshStandardMaterial();
  418. if ( params.color ) {
  419. material.color.set( params.color );
  420. }
  421. return material;
  422. };
  423. const setPosition = ( object, position ) => {
  424. if ( position ) {
  425. object.position.set(
  426. position.x ?? 0,
  427. position.y ?? 0,
  428. position.z ?? 0
  429. );
  430. }
  431. };
  432. switch ( commandData.type ) {
  433. case 'AddObject':
  434. const type = commandData.params.type?.toLowerCase();
  435. if ( type === 'box' || type === 'cube' ) {
  436. const width = commandData.params.width ?? 1;
  437. const height = commandData.params.height ?? 1;
  438. const depth = commandData.params.depth ?? 1;
  439. const widthSegments = commandData.params.widthSegments ?? 1;
  440. const heightSegments = commandData.params.heightSegments ?? 1;
  441. const depthSegments = commandData.params.depthSegments ?? 1;
  442. const geometry = new BoxGeometry( width, height, depth, widthSegments, heightSegments, depthSegments );
  443. const mesh = new Mesh( geometry, createMaterial( commandData.params ) );
  444. mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Box' );
  445. setPosition( mesh, commandData.params.position );
  446. command = new Commands.AddObjectCommand( this.editor, mesh );
  447. this.lastModifiedObject = mesh;
  448. } else if ( type === 'sphere' ) {
  449. const radius = commandData.params.radius ?? 0.5;
  450. const widthSegments = commandData.params.widthSegments ?? 32;
  451. const heightSegments = commandData.params.heightSegments ?? 16;
  452. const geometry = new SphereGeometry( radius, widthSegments, heightSegments );
  453. const mesh = new Mesh( geometry, createMaterial( commandData.params ) );
  454. mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Sphere' );
  455. setPosition( mesh, commandData.params.position );
  456. command = new Commands.AddObjectCommand( this.editor, mesh );
  457. this.lastModifiedObject = mesh;
  458. } else if ( type === 'cylinder' ) {
  459. const radiusTop = commandData.params.radiusTop ?? 0.5;
  460. const radiusBottom = commandData.params.radiusBottom ?? 0.5;
  461. const height = commandData.params.height ?? 1;
  462. const radialSegments = commandData.params.radialSegments ?? 32;
  463. const heightSegments = commandData.params.heightSegments ?? 1;
  464. const openEnded = commandData.params.openEnded ?? false;
  465. const geometry = new CylinderGeometry( radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded );
  466. const mesh = new Mesh( geometry, createMaterial( commandData.params ) );
  467. mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Cylinder' );
  468. setPosition( mesh, commandData.params.position );
  469. command = new Commands.AddObjectCommand( this.editor, mesh );
  470. this.lastModifiedObject = mesh;
  471. } else if ( type === 'directionallight' ) {
  472. const color = commandData.params.color || 0xffffff;
  473. const intensity = commandData.params.intensity ?? 1;
  474. const light = new DirectionalLight( color, intensity );
  475. light.name = commandData.params.name || this.generateUniqueObjectName( 'DirectionalLight' );
  476. setPosition( light, commandData.params.position );
  477. command = new Commands.AddObjectCommand( this.editor, light );
  478. this.lastModifiedObject = light;
  479. } else if ( type === 'pointlight' ) {
  480. const color = commandData.params.color || 0xffffff;
  481. const intensity = commandData.params.intensity ?? 1;
  482. const distance = commandData.params.distance ?? 0;
  483. const decay = commandData.params.decay ?? 2;
  484. const light = new PointLight( color, intensity, distance, decay );
  485. light.name = commandData.params.name || this.generateUniqueObjectName( 'PointLight' );
  486. setPosition( light, commandData.params.position );
  487. command = new Commands.AddObjectCommand( this.editor, light );
  488. this.lastModifiedObject = light;
  489. } else if ( type === 'ambientlight' ) {
  490. const color = commandData.params.color || 0xffffff;
  491. const intensity = commandData.params.intensity ?? 1;
  492. const light = new AmbientLight( color, intensity );
  493. light.name = commandData.params.name || this.generateUniqueObjectName( 'AmbientLight' );
  494. command = new Commands.AddObjectCommand( this.editor, light );
  495. this.lastModifiedObject = light;
  496. } else {
  497. console.warn( 'AGENT: Unsupported object type:', type );
  498. }
  499. break;
  500. case 'SetPosition':
  501. const positionObject = getTargetObject( commandData.params.object );
  502. if ( positionObject && commandData.params.position ) {
  503. const currentPos = positionObject.position;
  504. const newPosition = new Vector3(
  505. commandData.params.position.x ?? currentPos.x,
  506. commandData.params.position.y ?? currentPos.y,
  507. commandData.params.position.z ?? currentPos.z
  508. );
  509. command = new Commands.SetPositionCommand( this.editor, positionObject, newPosition );
  510. }
  511. break;
  512. case 'SetRotation':
  513. const rotationObject = getTargetObject( commandData.params.object );
  514. if ( rotationObject && commandData.params.rotation ) {
  515. const rot = commandData.params.rotation;
  516. const currentRot = rotationObject.rotation;
  517. const newRotation = new Vector3(
  518. rot.x ?? currentRot.x,
  519. rot.y ?? currentRot.y,
  520. rot.z ?? currentRot.z
  521. );
  522. command = new Commands.SetRotationCommand( this.editor, rotationObject, newRotation );
  523. }
  524. break;
  525. case 'SetScale':
  526. const scaleObject = getTargetObject( commandData.params.object );
  527. if ( scaleObject && commandData.params.scale ) {
  528. const scale = commandData.params.scale;
  529. const newScale = new Vector3( scale.x || 1, scale.y || 1, scale.z || 1 );
  530. command = new Commands.SetScaleCommand( this.editor, scaleObject, newScale );
  531. }
  532. break;
  533. case 'SetMaterialColor':
  534. const colorObject = getTargetObject( commandData.params.object );
  535. if ( colorObject && colorObject.material && commandData.params.color ) {
  536. let colorValue = commandData.params.color;
  537. // If color is "random", generate a random color
  538. if ( colorValue === 'random' ) {
  539. colorValue = this.generateRandomColor();
  540. }
  541. const color = new Color( colorValue );
  542. command = new Commands.SetMaterialColorCommand( this.editor, colorObject, 'color', color.getHex() );
  543. }
  544. break;
  545. case 'SetMaterialValue':
  546. const materialObject = getTargetObject( commandData.params.object );
  547. if ( materialObject && materialObject.material && commandData.params.property ) {
  548. const property = commandData.params.property;
  549. let value = commandData.params.value;
  550. // Handle special cases for certain property types
  551. if ( property.includes( 'map' ) && value === null ) {
  552. // Handle removing textures
  553. value = null;
  554. } else if ( typeof value === 'string' && !isNaN( value ) ) {
  555. // Convert numeric strings to numbers
  556. value = parseFloat( value );
  557. }
  558. command = new Commands.SetMaterialValueCommand( this.editor, materialObject, property, value );
  559. }
  560. break;
  561. case 'SetGeometry':
  562. const detailObject = getTargetObject( commandData.params.object );
  563. if ( detailObject && detailObject.geometry ) {
  564. const params = commandData.params;
  565. let newGeometry;
  566. if ( detailObject.geometry instanceof BoxGeometry ) {
  567. const box = detailObject.geometry;
  568. newGeometry = new BoxGeometry(
  569. box.parameters.width ?? 1,
  570. box.parameters.height ?? 1,
  571. box.parameters.depth ?? 1,
  572. params.widthSegments ?? 1,
  573. params.heightSegments ?? 1,
  574. params.depthSegments ?? 1
  575. );
  576. } else if ( detailObject.geometry instanceof SphereGeometry ) {
  577. const sphere = detailObject.geometry;
  578. newGeometry = new SphereGeometry(
  579. sphere.parameters.radius ?? 0.5,
  580. params.widthSegments ?? 32,
  581. params.heightSegments ?? 16
  582. );
  583. } else if ( detailObject.geometry instanceof CylinderGeometry ) {
  584. const cylinder = detailObject.geometry;
  585. newGeometry = new CylinderGeometry(
  586. params.radiusTop ?? cylinder.parameters.radiusTop ?? 0.5,
  587. params.radiusBottom ?? cylinder.parameters.radiusBottom ?? 0.5,
  588. params.height ?? cylinder.parameters.height ?? 1,
  589. params.radialSegments ?? cylinder.parameters.radialSegments ?? 32,
  590. params.heightSegments ?? cylinder.parameters.heightSegments ?? 1,
  591. params.openEnded ?? cylinder.parameters.openEnded ?? false
  592. );
  593. }
  594. if ( newGeometry ) {
  595. command = new Commands.SetGeometryCommand( this.editor, detailObject, newGeometry );
  596. }
  597. }
  598. break;
  599. case 'RemoveObject':
  600. const removeObject = getTargetObject( commandData.params.object );
  601. if ( removeObject ) {
  602. command = new Commands.RemoveObjectCommand( this.editor, removeObject );
  603. this.lastModifiedObject = null;
  604. }
  605. break;
  606. case 'MultiCmds':
  607. if ( Array.isArray( commandData.params.commands ) ) {
  608. const commands = [];
  609. for ( const cmd of commandData.params.commands ) {
  610. const subCommand = await this.executeCommand( cmd );
  611. if ( subCommand ) commands.push( subCommand );
  612. }
  613. command = new Commands.MultiCmdsCommand( this.editor, commands );
  614. }
  615. break;
  616. default:
  617. console.warn( 'AGENT: Unsupported command type:', commandData.type, '- Available commands are: AddObject, SetPosition, SetRotation, SetScale, SetMaterialColor, SetMaterialValue, SetGeometry, RemoveObject, MultiCmds' );
  618. break;
  619. }
  620. console.log( 'AGENT: Command:', command );
  621. if ( command ) {
  622. this.editor.execute( command );
  623. }
  624. return command;
  625. }
  626. getSceneInfo() {
  627. const scene = this.editor.scene;
  628. // Helper to get all objects of a specific type
  629. const getObjectsByType = ( type ) => {
  630. return scene.children.filter( obj => {
  631. const baseName = obj.name.replace( /\d+$/, '' );
  632. return baseName.toLowerCase() === type.toLowerCase();
  633. } ).map( obj => obj.name );
  634. };
  635. // Get base names and their counts
  636. const nameCount = {};
  637. const objectsByType = {};
  638. scene.children.forEach( obj => {
  639. const baseName = obj.name.replace( /\d+$/, '' ); // Remove trailing numbers
  640. nameCount[ baseName ] = ( nameCount[ baseName ] || 0 ) + 1;
  641. // Group objects by their base name
  642. if ( ! objectsByType[ baseName ] ) {
  643. objectsByType[ baseName ] = [];
  644. }
  645. objectsByType[ baseName ].push( obj.name );
  646. } );
  647. const objects = scene.children.map( obj => ( {
  648. type: obj.type,
  649. name: obj.name,
  650. baseName: obj.name.replace( /\d+$/, '' ), // Add base name
  651. position: obj.position,
  652. rotation: obj.rotation,
  653. scale: obj.scale,
  654. isMesh: obj.isMesh,
  655. isLight: obj.isLight,
  656. material: obj.material ? {
  657. type: obj.material.type,
  658. color: obj.material.color ? '#' + obj.material.color.getHexString() : undefined
  659. } : undefined
  660. } ) );
  661. return {
  662. objects,
  663. meshes: objects.filter( obj => obj.isMesh ),
  664. lights: objects.filter( obj => obj.isLight ),
  665. materials: Object.keys( this.editor.materials ).length,
  666. cameras: Object.keys( this.editor.cameras ).length,
  667. objectCounts: nameCount, // Add counts of similar objects
  668. objectsByType, // Add grouped objects by type
  669. spheres: getObjectsByType( 'Sphere' ),
  670. boxes: getObjectsByType( 'Box' ),
  671. cylinders: getObjectsByType( 'Cylinder' ),
  672. directionalLights: getObjectsByType( 'DirectionalLight' ),
  673. pointLights: getObjectsByType( 'PointLight' ),
  674. ambientLights: getObjectsByType( 'AmbientLight' )
  675. };
  676. }
  677. clear() {
  678. while ( this.container.children.length > 0 ) {
  679. this.container.remove( this.container.children[ 0 ] );
  680. }
  681. }
  682. }
  683. export { Agent };
粤ICP备19079148号