Agent.js 26 KB

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